Skip to content

ElliePHP Framework - Complete Guide

Version: 1.0.0
Framework Version: 1.0.0
PHP Version Required: 8.4+
Last Updated: November 17, 2024
License: MIT


About This Documentation

This comprehensive guide provides complete documentation for the ElliePHP Framework, a fast and modular PHP microframework focused on clean architecture, performance, and zero-bloat components. The documentation is organized to serve both as a learning resource for new developers and a reference guide for experienced users.

The guide covers all framework features including routing, dependency injection, middleware, caching, console commands, HTTP handling, logging, and utilities. Each section includes practical examples, API references, and best practices to help you build robust applications with ElliePHP.


Table of Contents

1. Introduction & Overview

2. Getting Started

3. Routing

4. Dependency Injection

5. Middleware

6. HTTP Request & Response

7. Caching

8. Console Commands

9. Logging

10. Utilities & Helpers

11. Configuration

12. Advanced Topics

13. API Reference


1. Introduction & Overview

1.1 What is ElliePHP?

ElliePHP is a fast, modular PHP microframework designed for developers who value clean architecture, performance, and simplicity. Built with modern PHP 8.4+ features, ElliePHP provides a solid foundation for building web applications and APIs without the overhead and complexity of larger frameworks.

Design Philosophy

ElliePHP is built on three core principles:

1. Zero-Bloat Architecture

ElliePHP follows a "zero-bloat" philosophy, meaning every component serves a clear purpose and nothing is included that you don't need. Unlike monolithic frameworks that bundle dozens of features you may never use, ElliePHP gives you exactly what you need to build modern web applications:

  • Routing with PSR-7 HTTP messages
  • Dependency injection with PSR-11 container
  • Middleware pipeline with PSR-15 handlers
  • Flexible caching with PSR-16 simple cache
  • Console commands for CLI tasks
  • Logging with PSR-3 interface

Each component is carefully selected and integrated to work seamlessly together while maintaining a minimal footprint. This approach results in faster bootstrap times, lower memory usage, and easier debugging.

2. Clean Architecture

ElliePHP encourages clean, maintainable code through:

  • Separation of Concerns: Clear boundaries between routing, business logic, and data access
  • Dependency Injection: Automatic constructor injection eliminates manual wiring and promotes testability
  • PSR Standards Compliance: Adherence to PHP-FIG standards ensures interoperability and best practices
  • Type Safety: Full PHP 8.4 type declarations for better IDE support and fewer runtime errors
  • Explicit Over Implicit: Clear, readable code over magic methods and hidden behavior

The framework's structure guides you toward organizing code in a way that's easy to understand, test, and maintain as your application grows.

3. Modular Component Approach

ElliePHP is built from independent, focused components that work together harmoniously:

  • ElliePHP/Routing: Fast HTTP routing with parameter extraction and middleware support
  • ElliePHP/Cache: Multi-driver caching (File, Redis, SQLite, APCu) with PSR-16 compliance
  • ElliePHP/Console: Symfony Console-based CLI with custom command support
  • ElliePHP/Support: Utility classes and helper functions for common tasks

Each component can be understood and used independently, making it easier to learn the framework incrementally. You can start with basic routing and gradually adopt more features as your application needs grow.

When to Use ElliePHP

ElliePHP is ideal for:

  • RESTful APIs: Build fast, lightweight APIs with JSON responses and middleware authentication
  • Microservices: Small, focused services that need quick startup and low overhead
  • Prototypes: Rapid development without framework complexity getting in the way
  • Learning: Understanding modern PHP patterns and PSR standards in a clean codebase
  • Custom Applications: Projects that need flexibility without framework constraints

ElliePHP may not be the best choice for:

  • Large monolithic applications requiring extensive built-in features (ORM, templating, authentication)
  • Projects requiring a large ecosystem of pre-built packages and integrations
  • Teams unfamiliar with dependency injection and PSR standards

Philosophy in Practice

The ElliePHP philosophy manifests in practical ways:

php
// Clean, explicit routing
Router::get('/users/{id}', [UserController::class, 'show']);

// Automatic dependency injection - no manual wiring
final readonly class UserController
{
    public function __construct(
        private UserService $userService,
        private CacheInterface $cache
    ) {}
    
    public function show(int $id): ResponseInterface
    {
        return response()->json([
            'user' => $this->userService->find($id)
        ]);
    }
}

// Simple, powerful helper functions
cache()->set('user:' . $id, $user, 3600);
report()->info('User accessed', ['id' => $id]);

No magic, no hidden behavior - just clean, readable PHP that does exactly what it says.

1.2 Key Features

ElliePHP provides a comprehensive set of features for building modern PHP applications while maintaining its zero-bloat philosophy.

Core Features

HTTP Routing

  • Fast, flexible routing with support for all HTTP methods (GET, POST, PUT, DELETE, PATCH, OPTIONS)
  • Dynamic route parameters with automatic type casting
  • Route middleware for authentication, logging, and request processing
  • Controller-based and closure-based route handlers
  • Built-in route listing command for debugging

Dependency Injection Container

  • Automatic constructor injection in controllers, middleware, and services
  • Interface-to-implementation binding for flexible architecture
  • Factory definitions for complex object creation
  • Singleton services with lazy loading
  • Production container compilation for optimal performance
  • PSR-11 Container Interface compliance

PSR-15 Middleware Pipeline

  • Standard PSR-15 middleware interface for request/response processing
  • Global middleware registration with execution order control
  • Route-specific middleware support
  • Built-in CORS and logging middleware
  • Automatic dependency injection in middleware constructors

Multi-Driver Caching

  • Four cache drivers: File, Redis, SQLite, and APCu
  • PSR-16 Simple Cache Interface compliance
  • TTL support with seconds or DateInterval
  • Batch operations (getMultiple, setMultiple, deleteMultiple)
  • Cache statistics and utilities
  • Easy driver switching via configuration

Console Commands

  • Symfony Console-based CLI application
  • Built-in commands: serve, cache:clear, routes, make:controller
  • Custom command creation with BaseCommand extension
  • Interactive prompts (ask, confirm, choice)
  • Rich output formatting (tables, colors, sections)
  • Automatic dependency injection in commands

HTTP Request & Response

  • PSR-7 HTTP Message Interface compliance
  • Type-safe request input with automatic casting (string, int, bool)
  • Fluent response builders for JSON, XML, HTML, and text
  • Status code helpers (ok, created, notFound, serverError, etc.)
  • Redirect helpers (redirect, back, redirectPermanent)
  • File download and streaming support
  • Header and cookie management

Logging

  • PSR-3 Logger Interface compliance via Monolog
  • Multiple log levels (debug, info, warning, error, critical)
  • Structured logging with context arrays
  • Separate channels for application and exception logs
  • Correlation ID tracking for request tracing
  • Automatic exception logging

Utilities & Helpers

  • String utilities (case conversion, validation, manipulation)
  • File operations (read, write, JSON handling, directory management)
  • JSON utilities with error handling and dot notation access
  • Hash utilities (password hashing, UUID, ULID, Nanoid generation)
  • Environment variable management with automatic type casting
  • Path helpers for common directory access

PSR Standards Compliance

ElliePHP adheres to PHP-FIG standards for maximum interoperability and best practices:

PSR-7: HTTP Message Interface

  • Standard request and response objects via Laminas Diactoros
  • Immutable message objects for predictable behavior
  • Stream-based body handling for memory efficiency
  • URI and uploaded file abstractions

PSR-11: Container Interface

  • Standard container interface via PHP-DI
  • get() and has() methods for service resolution
  • Interoperable with any PSR-11 compatible library
  • Enables easy testing with container mocking

PSR-15: HTTP Server Request Handlers

  • Standard middleware interface for request processing
  • process() method with request and handler parameters
  • Middleware pipeline with proper delegation
  • Compatible with any PSR-15 middleware library

PSR-16: Simple Cache

  • Standard cache interface via ElliePHP/Cache
  • Simple get/set/delete/clear operations
  • TTL support with flexible time formats
  • Multiple driver implementations

Performance Characteristics

ElliePHP is designed for speed and efficiency:

Fast Bootstrap

  • Minimal framework overhead with lazy loading
  • Container compilation in production eliminates reflection
  • Route caching for instant route matching
  • Optimized autoloading with Composer

Low Memory Footprint

  • Zero-bloat architecture means only necessary code is loaded
  • Efficient PSR-7 stream handling for large responses
  • Optional APCu caching for in-memory performance
  • Singleton services prevent duplicate instantiation

Production Optimizations

  • Container compilation generates optimized PHP code
  • Route caching eliminates runtime route parsing
  • Configurable cache drivers for different performance needs
  • Proxy generation for lazy-loaded services

Benchmarks

  • Sub-millisecond routing for simple requests
  • Minimal memory usage (~2-4MB for basic requests)
  • Scales efficiently with request complexity
  • Production container compilation reduces overhead by ~40%

PHP 8.4 Type Safety Features

ElliePHP leverages modern PHP 8.4 features for better code quality:

Strict Type Declarations

php
// All framework code uses strict types
declare(strict_types=1);

// Type-safe method signatures
public function find(int $id): ?User
{
    return $this->repository->find($id);
}

Readonly Classes

php
// Immutable controllers and services
final readonly class UserController
{
    public function __construct(
        private UserService $service,
        private CacheInterface $cache
    ) {}
}

Property Type Declarations

php
// Typed properties prevent runtime errors
private string $name;
private int $age;
private ?DateTime $createdAt = null;

Union and Intersection Types

php
// Flexible type definitions
public function process(string|int $id): User|null
{
    // ...
}

Named Arguments

php
// Clear, self-documenting function calls
response()->json(
    data: ['users' => $users],
    status: 200,
    headers: ['X-Total-Count' => count($users)]
);

Attributes for Metadata

php
// Clean metadata without docblocks
#[Route('/api/users', methods: ['GET'])]
#[Middleware(AuthMiddleware::class)]
public function index(): ResponseInterface
{
    // ...
}

These type safety features result in:

  • Fewer runtime errors caught at development time
  • Better IDE autocomplete and refactoring support
  • Self-documenting code that's easier to understand
  • Improved performance through type optimizations

1.3 System Requirements

ElliePHP has minimal system requirements, making it easy to deploy on most modern hosting environments.

PHP Version

Required: PHP 8.4 or higher

ElliePHP requires PHP 8.4+ to take advantage of modern language features including:

  • Readonly classes for immutable objects
  • Property hooks for cleaner accessors
  • Enhanced type system with generics support
  • Performance improvements and optimizations
  • Security enhancements

Required PHP Extensions

The following PHP extensions must be enabled:

ext-pdo (Required)

  • PDO (PHP Data Objects) extension for database abstraction
  • Required for SQLite cache driver
  • Typically included in standard PHP installations

ext-json (Required)

  • JSON encoding and decoding support
  • Required for JSON responses and configuration parsing
  • Included by default in PHP 8.4+

ext-mbstring (Recommended)

  • Multibyte string handling for international character support
  • Used by string utilities for proper UTF-8 handling
  • Highly recommended for production applications

Optional PHP Extensions

These extensions enable additional features:

ext-redis (Optional)

  • Required only if using Redis cache driver
  • Install via: pecl install redis
  • Provides high-performance in-memory caching

ext-apcu (Optional)

  • Required only if using APCu cache driver
  • Install via: pecl install apcu
  • Provides shared memory caching for single-server deployments

ext-opcache (Recommended)

  • PHP opcode caching for improved performance
  • Highly recommended for production environments
  • Typically included in PHP installations

Composer Dependencies

ElliePHP uses Composer for dependency management. The following packages are required:

Core Dependencies

json
{
  "php": "^8.4",
  "ext-pdo": "*",
  "elliephp/cache": "^1.0",
  "elliephp/console": "^1.0",
  "elliephp/routing": "^1.0",
  "elliephp/support": "^1.0",
  "fig/http-message-util": "^1.1",
  "laminas/laminas-diactoros": "^3.8",
  "laminas/laminas-httphandlerrunner": "^2.13",
  "monolog/monolog": "^3.9",
  "php-di/php-di": "^7.1",
  "symfony/process": "^7.3"
}

ElliePHP Components

  • elliephp/cache (^1.0): Multi-driver caching with PSR-16 compliance
  • elliephp/console (^1.0): Console command framework based on Symfony Console
  • elliephp/routing (^1.0): Fast HTTP routing with middleware support
  • elliephp/support (^1.0): Utility classes and helper functions

Third-Party Dependencies

  • fig/http-message-util (^1.1): HTTP message utilities and status code constants
  • laminas/laminas-diactoros (^3.8): PSR-7 HTTP message implementation
  • laminas/laminas-httphandlerrunner (^2.13): PSR-15 request handler runner
  • monolog/monolog (^3.9): PSR-3 logging implementation
  • php-di/php-di (^7.1): PSR-11 dependency injection container
  • symfony/process (^7.3): Process execution for console commands

Development Dependencies

json
{
  "phpunit/phpunit": "^11.0",
  "rector/rector": "^2.2",
  "roave/security-advisories": "dev-latest",
  "symfony/var-dumper": "^7.3"
}
  • phpunit/phpunit: Unit testing framework
  • rector/rector: Automated code refactoring and upgrades
  • roave/security-advisories: Prevents installation of packages with known security vulnerabilities
  • symfony/var-dumper: Enhanced debugging and variable dumping

Server Requirements

Web Server

Any web server capable of running PHP:

  • Apache 2.4+ with mod_rewrite
  • Nginx 1.18+
  • PHP built-in development server (for development only)
  • Caddy, Lighttpd, or other modern web servers

Operating System

ElliePHP runs on any operating system that supports PHP 8.4:

  • Linux (Ubuntu, Debian, CentOS, Alpine, etc.)
  • macOS 12+
  • Windows 10/11 with PHP installed
  • Docker containers with PHP 8.4 images

Memory

  • Minimum: 128MB PHP memory_limit
  • Recommended: 256MB or higher for production
  • Development: 512MB for comfortable development with debugging tools

Disk Space

  • Framework: ~5MB (including vendor dependencies)
  • Cache Storage: Varies based on cache usage (typically 10-100MB)
  • Logs: Varies based on logging verbosity (typically 10-50MB)

Cache Driver Requirements

Different cache drivers have specific requirements:

File Cache (Default)

  • No additional requirements
  • Requires writable storage/Cache directory
  • Works on all systems

Redis Cache

  • Redis server 5.0+ running and accessible
  • ext-redis PHP extension installed
  • Network connectivity to Redis server

SQLite Cache

  • ext-pdo and ext-pdo_sqlite extensions (included by default)
  • Writable storage/Cache directory
  • Works on all systems

APCu Cache

  • ext-apcu PHP extension installed
  • APCu enabled in php.ini
  • Single-server deployments only (not shared across servers)

Checking Your Environment

You can verify your environment meets the requirements:

Check PHP Version

bash
php -v
# Should show PHP 8.4.0 or higher

Check Required Extensions

bash
php -m | grep -E "pdo|json|mbstring"
# Should list: PDO, json, mbstring

Check Optional Extensions

bash
php -m | grep -E "redis|apcu|opcache"
# Lists installed optional extensions

Verify Composer

bash
composer --version
# Should show Composer 2.0 or higher

Production Environment Recommendations

For production deployments, we recommend:

  1. PHP Configuration

    • Enable OPcache for optimal performance
    • Set appropriate memory_limit (256MB+)
    • Disable display_errors, enable error_logging
    • Set appropriate upload_max_filesize and post_max_size
  2. Web Server

    • Enable HTTP/2 for better performance
    • Configure proper SSL/TLS certificates
    • Set up gzip/brotli compression
    • Configure appropriate timeout values
  3. Caching

    • Use Redis or APCu for production caching
    • Enable container compilation (APP_ENV=production)
    • Configure route caching
    • Enable OPcache with appropriate settings
  4. Security

    • Keep PHP and dependencies updated
    • Use roave/security-advisories to prevent vulnerable packages
    • Configure proper file permissions
    • Enable HTTPS/TLS encryption
    • Implement rate limiting and CORS policies
  5. Monitoring

    • Configure log rotation for application logs
    • Set up error monitoring and alerting
    • Monitor cache hit rates and performance
    • Track memory usage and response times

1.4 Architecture Overview

[Content to be added]


2. Getting Started

2.1 Installation

ElliePHP uses Composer for dependency management and installation. Follow these steps to get started with a new ElliePHP project.

Prerequisites

Before installing ElliePHP, ensure you have:

  • PHP 8.4 or higher installed
  • Composer 2.0 or higher installed
  • Required PHP extensions: ext-pdo, ext-json
  • A terminal/command line interface

Verify your PHP version:

bash
php -v
# Should show PHP 8.4.0 or higher

Verify Composer is installed:

bash
composer --version
# Should show Composer version 2.0 or higher

Installation Steps

1. Clone or Download the Framework

Clone the ElliePHP framework repository:

bash
git clone https://github.com/elliephp/framework.git my-project
cd my-project

Or download and extract the framework archive to your project directory.

2. Install Dependencies

Install all required dependencies using Composer:

bash
composer install

This will install:

  • ElliePHP core components (routing, cache, console, support)
  • PSR-7 HTTP message implementation (Laminas Diactoros)
  • PSR-11 dependency injection container (PHP-DI)
  • PSR-3 logging implementation (Monolog)
  • Additional required packages

The installation typically takes 30-60 seconds depending on your internet connection.

3. Configure Environment Variables

Copy the example environment file to create your configuration:

bash
cp .env.example .env

Open the .env file and configure your application settings:

env
# Application Configuration
APP_NAME='ElliePHP'
APP_DEBUG=true
APP_TIMEZONE='UTC'

# Cache Configuration
# Supported drivers: file, redis, sqlite, apcu
CACHE_DRIVER=file

# Redis Configuration (when using redis cache driver)
REDIS_HOST='127.0.0.1'
REDIS_PORT=6379
REDIS_PASSWORD=null
REDIS_DATABASE=0
REDIS_TIMEOUT=5

Key Configuration Options:

  • APP_NAME: Your application name (used in logs and responses)
  • APP_DEBUG: Enable debug mode for development (set to false in production)
  • APP_TIMEZONE: Default timezone for date/time operations
  • CACHE_DRIVER: Cache driver to use (file, redis, sqlite, or apcu)

For development, the default settings work out of the box. For production, see the Production Optimization section.

4. Set Directory Permissions

Ensure the storage directory is writable:

bash
chmod -R 775 storage

The storage directory is used for:

  • Cache files (when using file cache driver)
  • Application logs
  • Temporary files

5. Verify Installation

Test that everything is working by starting the development server:

bash
php ellie serve

Then visit http://127.0.0.1:8000 in your browser. You should see a JSON response:

json
{
  "message": "Welcome to ElliePHP Microframework",
  "version": "1.0.0",
  "docs": "https://github.com/elliephp"
}

If you see this response, congratulations! ElliePHP is successfully installed.

Optional: Install Additional Cache Drivers

Depending on your caching needs, you may want to install additional PHP extensions:

Redis Cache Driver

bash
pecl install redis

Then enable the extension in your php.ini:

ini
extension=redis.so

APCu Cache Driver

bash
pecl install apcu

Then enable the extension in your php.ini:

ini
extension=apcu.so
apc.enabled=1
apc.shm_size=32M

Troubleshooting Installation

Composer Install Fails

If composer install fails with memory errors:

bash
php -d memory_limit=-1 /usr/local/bin/composer install

Permission Denied Errors

If you encounter permission errors on the storage directory:

bash
sudo chown -R $USER:$USER storage
chmod -R 775 storage

Missing PHP Extensions

If you're missing required extensions, install them based on your system:

Ubuntu/Debian:

bash
sudo apt-get install php8.4-pdo php8.4-json php8.4-mbstring

macOS (using Homebrew):

bash
brew install php@8.4

Port Already in Use

If port 8000 is already in use when starting the dev server:

bash
php ellie serve --port=8080

2.2 Directory Structure

ElliePHP follows a clean, organized directory structure that separates concerns and makes it easy to locate files. Understanding this structure will help you navigate the framework and organize your application code effectively.

Root Directory Structure

elliephp-framework/
├── app/                    # Application code
├── configs/                # Configuration files
├── public/                 # Web server document root
├── routes/                 # Route definitions
├── src/                    # Framework core code
├── storage/                # Writable storage (cache, logs)
├── tests/                  # Test files
├── vendor/                 # Composer dependencies
├── .env                    # Environment configuration
├── .env.example            # Example environment file
├── composer.json           # Composer dependencies
├── composer.lock           # Locked dependency versions
├── ellie                   # Console application entry point
└── README.md               # Project documentation

The app/ Directory

The app/ directory contains your application-specific code. This is where you'll spend most of your development time.

app/
├── Console/
│   └── Command/           # Custom console commands
│       └── ServeCommand.php
├── Http/
│   ├── Controllers/       # HTTP controllers
│   │   └── WelcomeController.php
│   └── Middleware/        # Custom middleware
└── Services/              # Business logic services

Purpose of Each Subdirectory:

  • Console/Command/: Custom CLI commands that extend the ellie console application
  • Http/Controllers/: Controller classes that handle HTTP requests and return responses
  • Http/Middleware/: Custom middleware for request/response processing
  • Services/: Business logic and service layer classes

Namespace Convention:

All classes in the app/ directory use the ElliePHP\Framework\Application\ namespace:

php
namespace ElliePHP\Framework\Application\Http\Controllers;
namespace ElliePHP\Framework\Application\Console\Command;
namespace ElliePHP\Framework\Application\Services;

The configs/ Directory

Configuration files that define how your application behaves.

configs/
├── app.php                # Application configuration
├── cache.php              # Cache driver configuration
├── container.php          # Dependency injection bindings
└── middleware.php         # Global middleware registration

Configuration Files:

  • app.php: Core application settings (name, debug mode, timezone)
  • cache.php: Cache driver configuration and connection settings
  • container.php: Service bindings and dependency injection configuration
  • middleware.php: Global middleware stack registration

Configuration files return PHP arrays and can access environment variables using the env() helper:

php
return [
    'debug' => env('APP_DEBUG', false),
    'timezone' => env('APP_TIMEZONE', 'UTC'),
];

The public/ Directory

The web server document root. This is the only directory that should be publicly accessible.

public/
├── index.php              # Application entry point
└── .htaccess              # Apache rewrite rules (if using Apache)

Important:

  • Point your web server's document root to this directory
  • index.php bootstraps the framework and handles all HTTP requests
  • Static assets (CSS, JS, images) can be placed here if needed

The routes/ Directory

Route definitions that map URLs to controllers or closures.

routes/
└── router.php             # HTTP route definitions

Route File:

The router.php file defines all HTTP routes for your application:

php
use ElliePHP\Components\Routing\Router;
use ElliePHP\Framework\Application\Http\Controllers\WelcomeController;

Router::get('/', WelcomeController::class);
Router::post('/users', [UserController::class, 'store']);

All routes are automatically loaded when the application boots.

The src/ Directory

Framework core code. You typically won't need to modify files here.

src/
├── Kernel/                # Application kernel
│   ├── HttpApplication.php
│   └── ConsoleApplication.php
└── Support/               # Helper functions
    ├── helpers.php
    └── path.php

Framework Components:

  • Kernel/: Application bootstrapping and request handling
  • Support/: Global helper functions and utilities

The src/ directory uses the ElliePHP\Framework\ namespace.

The storage/ Directory

Writable storage for cache files, logs, and temporary data.

storage/
├── Cache/                 # File cache storage
│   └── .gitkeep
└── Logs/                  # Application logs
    ├── app.log
    └── exceptions.log

Storage Subdirectories:

  • Cache/: File-based cache storage (when using file cache driver)
  • Logs/: Application and exception logs

Permissions:

The storage/ directory must be writable by the web server:

bash
chmod -R 775 storage

The tests/ Directory

PHPUnit test files for testing your application.

tests/
├── Feature/               # Feature/integration tests
└── Unit/                  # Unit tests

Run tests using:

bash
composer test

The vendor/ Directory

Composer dependencies. This directory is auto-generated and should not be modified manually.

Important:

  • Never commit vendor/ to version control
  • Add vendor/ to your .gitignore file
  • Dependencies are installed via composer install

File Organization Best Practices

Controllers

Group related controllers in subdirectories:

app/Http/Controllers/
├── Api/
│   ├── UserController.php
│   └── PostController.php
└── Admin/
    └── DashboardController.php

Services

Organize services by domain:

app/Services/
├── User/
│   ├── UserService.php
│   └── UserRepository.php
└── Auth/
    └── AuthenticationService.php

Middleware

Keep middleware focused and single-purpose:

app/Http/Middleware/
├── AuthenticateMiddleware.php
├── RateLimitMiddleware.php
└── CorsMiddleware.php

Autoloading

ElliePHP uses PSR-4 autoloading configured in composer.json:

json
"autoload": {
    "psr-4": {
        "ElliePHP\\Framework\\": "src/",
        "ElliePHP\\Framework\\Application\\": "app/"
    },
    "files": [
        "src/Support/path.php",
        "src/Support/helpers.php"
    ]
}

Autoloading Rules:

  • Classes in src/ use the ElliePHP\Framework\ namespace
  • Classes in app/ use the ElliePHP\Framework\Application\ namespace
  • Helper functions are automatically loaded from src/Support/

After adding new classes or namespaces, regenerate the autoloader:

bash
composer dump-autoload

Adding New Directories

You can add custom directories to organize your code:

1. Create the directory:

bash
mkdir -p app/Repositories

2. Add classes with proper namespace:

php
<?php

namespace ElliePHP\Framework\Application\Repositories;

class UserRepository
{
    // ...
}

3. The PSR-4 autoloader will automatically find your classes

No additional configuration needed - the autoloader maps namespaces to directories automatically.

2.3 Your First Application

Let's build a simple API endpoint to understand how ElliePHP works. We'll create a user management endpoint that demonstrates routing, controllers, and responses.

Step 1: Create a Controller

Controllers handle incoming HTTP requests and return responses. Let's create a UserController:

Create the file: app/Http/Controllers/UserController.php

php
<?php

namespace ElliePHP\Framework\Application\Http\Controllers;

use Psr\Http\Message\ResponseInterface;

final readonly class UserController
{
    public function index(): ResponseInterface
    {
        $users = [
            ['id' => 1, 'name' => 'Alice Johnson', 'email' => 'alice@example.com'],
            ['id' => 2, 'name' => 'Bob Smith', 'email' => 'bob@example.com'],
            ['id' => 3, 'name' => 'Carol White', 'email' => 'carol@example.com'],
        ];

        return response()->json([
            'success' => true,
            'data' => $users,
            'count' => count($users)
        ]);
    }

    public function show(int $id): ResponseInterface
    {
        // Simulate fetching a user by ID
        $users = [
            1 => ['id' => 1, 'name' => 'Alice Johnson', 'email' => 'alice@example.com'],
            2 => ['id' => 2, 'name' => 'Bob Smith', 'email' => 'bob@example.com'],
            3 => ['id' => 3, 'name' => 'Carol White', 'email' => 'carol@example.com'],
        ];

        if (!isset($users[$id])) {
            return response()->json([
                'success' => false,
                'error' => 'User not found'
            ], 404);
        }

        return response()->json([
            'success' => true,
            'data' => $users[$id]
        ]);
    }

    public function store(): ResponseInterface
    {
        // Get request data
        $name = request()->input('name');
        $email = request()->input('email');

        // Validate input
        if (!$name || !$email) {
            return response()->json([
                'success' => false,
                'error' => 'Name and email are required'
            ], 400);
        }

        // Simulate creating a user
        $user = [
            'id' => 4,
            'name' => $name,
            'email' => $email,
            'created_at' => date('Y-m-d H:i:s')
        ];

        return response()->json([
            'success' => true,
            'message' => 'User created successfully',
            'data' => $user
        ], 201);
    }
}

Key Points:

  • Controllers are readonly classes for immutability
  • Methods return ResponseInterface (PSR-7 standard)
  • Use response()->json() helper to create JSON responses
  • Use request()->input() helper to access request data
  • Route parameters (like $id) are automatically injected

Step 2: Define Routes

Routes map URLs to controller methods. Open routes/router.php and add your routes:

php
<?php

use ElliePHP\Components\Routing\Router;
use ElliePHP\Framework\Application\Http\Controllers\WelcomeController;
use ElliePHP\Framework\Application\Http\Controllers\UserController;

// Welcome route
Router::get('/', WelcomeController::class);

// User routes
Router::get('/users', [UserController::class, 'index']);
Router::get('/users/{id}', [UserController::class, 'show']);
Router::post('/users', [UserController::class, 'store']);

Route Syntax:

  • Router::get($path, $handler): Define a GET route
  • Router::post($path, $handler): Define a POST route
  • {id}: Dynamic route parameter (automatically passed to controller method)
  • [Controller::class, 'method']: Controller method handler
  • Controller::class: Single-method controller (calls __invoke or default method)

Step 3: Test Your Endpoints

Start the development server:

bash
php ellie serve

Test the GET /users endpoint:

bash
curl http://127.0.0.1:8000/users

Response:

json
{
  "success": true,
  "data": [
    {"id": 1, "name": "Alice Johnson", "email": "alice@example.com"},
    {"id": 2, "name": "Bob Smith", "email": "bob@example.com"},
    {"id": 3, "name": "Carol White", "email": "carol@example.com"}
  ],
  "count": 3
}

Test the GET /users/{id} endpoint:

bash
curl http://127.0.0.1:8000/users/1

Response:

json
{
  "success": true,
  "data": {
    "id": 1,
    "name": "Alice Johnson",
    "email": "alice@example.com"
  }
}

Test with a non-existent user:

bash
curl http://127.0.0.1:8000/users/999

Response (404):

json
{
  "success": false,
  "error": "User not found"
}

Test the POST /users endpoint:

bash
curl -X POST http://127.0.0.1:8000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "David Brown", "email": "david@example.com"}'

Response (201):

json
{
  "success": true,
  "message": "User created successfully",
  "data": {
    "id": 4,
    "name": "David Brown",
    "email": "david@example.com",
    "created_at": "2024-11-17 10:30:45"
  }
}

Step 4: Add Dependency Injection (Optional)

Let's enhance our controller with dependency injection by adding a service layer.

Create a UserService: app/Services/UserService.php

php
<?php

namespace ElliePHP\Framework\Application\Services;

class UserService
{
    private array $users = [
        1 => ['id' => 1, 'name' => 'Alice Johnson', 'email' => 'alice@example.com'],
        2 => ['id' => 2, 'name' => 'Bob Smith', 'email' => 'bob@example.com'],
        3 => ['id' => 3, 'name' => 'Carol White', 'email' => 'carol@example.com'],
    ];

    public function all(): array
    {
        return array_values($this->users);
    }

    public function find(int $id): ?array
    {
        return $this->users[$id] ?? null;
    }

    public function create(string $name, string $email): array
    {
        $id = max(array_keys($this->users)) + 1;
        
        $user = [
            'id' => $id,
            'name' => $name,
            'email' => $email,
            'created_at' => date('Y-m-d H:i:s')
        ];

        $this->users[$id] = $user;

        return $user;
    }
}

Update the controller to use dependency injection:

php
<?php

namespace ElliePHP\Framework\Application\Http\Controllers;

use ElliePHP\Framework\Application\Services\UserService;
use Psr\Http\Message\ResponseInterface;

final readonly class UserController
{
    public function __construct(
        private UserService $userService
    ) {}

    public function index(): ResponseInterface
    {
        $users = $this->userService->all();

        return response()->json([
            'success' => true,
            'data' => $users,
            'count' => count($users)
        ]);
    }

    public function show(int $id): ResponseInterface
    {
        $user = $this->userService->find($id);

        if (!$user) {
            return response()->json([
                'success' => false,
                'error' => 'User not found'
            ], 404);
        }

        return response()->json([
            'success' => true,
            'data' => $user
        ]);
    }

    public function store(): ResponseInterface
    {
        $name = request()->input('name');
        $email = request()->input('email');

        if (!$name || !$email) {
            return response()->json([
                'success' => false,
                'error' => 'Name and email are required'
            ], 400);
        }

        $user = $this->userService->create($name, $email);

        return response()->json([
            'success' => true,
            'message' => 'User created successfully',
            'data' => $user
        ], 201);
    }
}

How Dependency Injection Works:

  • The UserService is automatically injected into the controller constructor
  • No manual instantiation or configuration needed
  • The container resolves dependencies automatically
  • Services can have their own dependencies injected too

Test the endpoints again - they work exactly the same, but now with cleaner separation of concerns!

Understanding the Request Flow

Here's what happens when a request comes in:

  1. Request arrives at public/index.php
  2. Router matches the URL to a route definition
  3. Container resolves the controller and injects dependencies
  4. Controller method is called with route parameters
  5. Response is returned and sent to the client
HTTP Request → Router → Container → Controller → Response → HTTP Response

Next Steps

Now that you've built your first application, you can:

  • Add more routes and controllers for different resources
  • Implement middleware for authentication or logging
  • Use caching to improve performance
  • Add custom console commands for maintenance tasks
  • Explore the full framework features in the following sections

2.4 Development Server

ElliePHP includes a built-in development server command that makes it easy to run your application locally without configuring Apache or Nginx.

Basic Usage

Start the development server with default settings:

bash
php ellie serve

This starts the server on http://127.0.0.1:8000 with the document root set to public/.

You'll see output like:

Starting development server on http://127.0.0.1:8000
Document root: public
Press Ctrl+C to stop the server

[Sun Nov 17 10:30:45 2024] PHP 8.4.0 Development Server (http://127.0.0.1:8000) started

The server will display request logs as you access your application:

[Sun Nov 17 10:31:12 2024] 127.0.0.1:54321 Accepted
[Sun Nov 17 10:31:12 2024] 127.0.0.1:54321 [200]: GET /users
[Sun Nov 17 10:31:12 2024] 127.0.0.1:54321 Closing

Command Options

The serve command supports several options to customize the server configuration:

Change the Host

bash
php ellie serve --host=0.0.0.0

This makes the server accessible from other devices on your network. Useful for testing on mobile devices or other computers.

Change the Port

bash
php ellie serve --port=8080

Or use the short option:

bash
php ellie serve -p 8080

Use this if port 8000 is already in use or you need to run multiple applications simultaneously.

Change the Document Root

bash
php ellie serve --docroot=public

Or use the short option:

bash
php ellie serve -d public

The document root is the directory that contains your index.php file. The default is public/.

Combine Multiple Options

bash
php ellie serve --host=0.0.0.0 --port=8080 --docroot=public

Or with short options:

bash
php ellie serve -p 8080 -d public

Command Reference

php ellie serve [options]

Options:
  --host=HOST        Server host (default: 127.0.0.1)
  -p, --port=PORT    Server port (default: 8000)
  -d, --docroot=DIR  Document root directory (default: public)
  -h, --help         Display help message

Common Use Cases

Local Development (Default)

bash
php ellie serve

Access at: http://127.0.0.1:8000

Testing on Mobile Devices

bash
php ellie serve --host=0.0.0.0

Access from other devices using your computer's IP address:

  • Find your IP: ifconfig (macOS/Linux) or ipconfig (Windows)
  • Access at: http://192.168.1.100:8000 (replace with your IP)

Running Multiple Applications

Terminal 1:

bash
cd project1
php ellie serve --port=8000

Terminal 2:

bash
cd project2
php ellie serve --port=8001

Access at:

  • Project 1: http://127.0.0.1:8000
  • Project 2: http://127.0.0.1:8001

Custom Port for Specific Services

bash
php ellie serve --port=3000

Useful if you're integrating with frontend frameworks that expect APIs on specific ports.

Stopping the Server

Press Ctrl+C in the terminal to stop the development server gracefully.

Important Notes

Development Only

The built-in PHP development server is designed for development and testing only. It is:

  • Single-threaded (handles one request at a time)
  • Not optimized for performance
  • Not suitable for production use
  • Lacks advanced features like SSL, load balancing, etc.

For production, use a proper web server like:

  • Apache with mod_php or PHP-FPM
  • Nginx with PHP-FPM
  • Caddy with PHP-FPM

Security Considerations

When using --host=0.0.0.0:

  • Your application becomes accessible to anyone on your network
  • Only use on trusted networks (home, office)
  • Never use on public networks without proper security measures
  • Consider using a firewall to restrict access

Performance

The development server:

  • Handles requests sequentially (one at a time)
  • May be slow with concurrent requests
  • Is sufficient for local development and testing
  • Should not be used for load testing or benchmarking

Troubleshooting

Port Already in Use

Error: Failed to listen on 127.0.0.1:8000

Solution: Use a different port

bash
php ellie serve --port=8080

Permission Denied

Error: Permission denied when starting server

Solution: Use a port above 1024 (ports below 1024 require root privileges)

bash
php ellie serve --port=8000  # OK
php ellie serve --port=80    # Requires sudo

Cannot Access from Other Devices

Problem: Server starts but can't access from phone/tablet

Solution: Use --host=0.0.0.0 and check firewall settings

bash
php ellie serve --host=0.0.0.0

Document Root Not Found

Error: Document root not found: public

Solution: Ensure you're in the project root directory or specify correct path

bash
cd /path/to/project
php ellie serve

Alternative: Using Other Web Servers

While the built-in server is convenient, you can also use other development servers:

PHP Built-in Server (Direct)

bash
php -S 127.0.0.1:8000 -t public

Using Docker

dockerfile
FROM php:8.4-cli
WORKDIR /app
COPY . /app
CMD ["php", "-S", "0.0.0.0:8000", "-t", "public"]

Using Laravel Valet (macOS)

bash
valet link
valet open

Using XAMPP/MAMP

Configure the document root to point to your public/ directory.


3. Routing

ElliePHP provides a fast, flexible routing system that maps HTTP requests to controllers or closures. Routes are defined in the routes/router.php file using the Router facade, which provides a clean, expressive API for defining your application's URL structure.

3.1 Basic Routing

The Router facade provides methods for all standard HTTP verbs: GET, POST, PUT, DELETE, and PATCH. Each method accepts a URL pattern and a handler (either a controller class, controller method array, or closure).

Available HTTP Methods

php
use ElliePHP\Components\Routing\Router;
use App\Http\Controllers\UserController;

// GET request
Router::get('/users', [UserController::class, 'index']);

// POST request
Router::post('/users', [UserController::class, 'store']);

// PUT request
Router::put('/users/{id}', [UserController::class, 'update']);

// DELETE request
Router::delete('/users/{id}', [UserController::class, 'destroy']);

// PATCH request
Router::patch('/users/{id}', [UserController::class, 'patch']);

Route File Location

All HTTP routes are defined in routes/router.php. This file is automatically loaded when your application boots:

php
<?php

/**
 * Application Routes
 *
 * Define your application routes here using the Router facade.
 */

use ElliePHP\Components\Routing\Router;
use ElliePHP\Framework\Application\Http\Controllers\WelcomeController;

Router::get('/', WelcomeController::class);
Router::get('/about', [PageController::class, 'about']);
Router::post('/contact', [ContactController::class, 'submit']);

Simple Route Examples

Homepage Route

php
Router::get('/', WelcomeController::class);

Static Pages

php
Router::get('/about', [PageController::class, 'about']);
Router::get('/contact', [PageController::class, 'contact']);
Router::get('/terms', [PageController::class, 'terms']);

RESTful API Routes

php
// List all users
Router::get('/api/users', [UserController::class, 'index']);

// Create new user
Router::post('/api/users', [UserController::class, 'store']);

// Get specific user
Router::get('/api/users/{id}', [UserController::class, 'show']);

// Update user
Router::put('/api/users/{id}', [UserController::class, 'update']);

// Delete user
Router::delete('/api/users/{id}', [UserController::class, 'destroy']);

Route Definition Syntax

Routes follow this basic pattern:

php
Router::{method}(string $url, Closure|callable|string|array $handler, array $options = [])

Parameters:

  • $url: The URL pattern (e.g., /users, /posts/{id})
  • $handler: Controller class, array, or closure to handle the request
  • $options: Optional array for middleware, name, etc. (advanced usage)

3.2 Route Parameters

Routes can include dynamic segments that capture values from the URL. These parameters are automatically extracted and passed to your controller methods.

Dynamic Route Segments

Define route parameters using curly braces {parameter}:

php
// Single parameter
Router::get('/users/{id}', [UserController::class, 'show']);

// Multiple parameters
Router::get('/posts/{postId}/comments/{commentId}', [CommentController::class, 'show']);

// Mixed static and dynamic segments
Router::get('/api/v1/users/{id}/profile', [ProfileController::class, 'show']);

Parameter Extraction

Route parameters are automatically extracted from the URL and made available to your controller:

php
final readonly class UserController
{
    public function show(int $id): ResponseInterface
    {
        // $id is automatically extracted from the URL
        // For URL: /users/123, $id will be 123
        
        return response()->json([
            'user_id' => $id,
            'user' => $this->userService->find($id)
        ]);
    }
}

Multiple Parameters Example

When your route has multiple parameters, they're extracted in order:

php
// Route definition
Router::get('/posts/{postId}/comments/{commentId}', [CommentController::class, 'show']);

// Controller method
final readonly class CommentController
{
    public function show(int $postId, int $commentId): ResponseInterface
    {
        // For URL: /posts/42/comments/7
        // $postId = 42
        // $commentId = 7
        
        $comment = $this->commentService->find($postId, $commentId);
        
        return response()->json(['comment' => $comment]);
    }
}

Parameter Type Casting

Parameters are automatically cast to the type specified in your controller method signature:

php
public function show(int $id): ResponseInterface
{
    // $id is automatically cast to integer
}

public function showBySlug(string $slug): ResponseInterface
{
    // $slug remains as string
}

Complex Parameter Examples

User Profile with Username

php
Router::get('/users/{username}/profile', [ProfileController::class, 'show']);

// Controller
public function show(string $username): ResponseInterface
{
    $user = $this->userService->findByUsername($username);
    return response()->json(['profile' => $user->profile]);
}

Nested Resources

php
Router::get('/categories/{categoryId}/products/{productId}', 
    [ProductController::class, 'show']);

// Controller
public function show(int $categoryId, int $productId): ResponseInterface
{
    $product = $this->productService->findInCategory($categoryId, $productId);
    return response()->json(['product' => $product]);
}

API Versioning with Parameters

php
Router::get('/api/v1/orders/{orderId}/items/{itemId}', 
    [OrderItemController::class, 'show']);

// Controller
public function show(int $orderId, int $itemId): ResponseInterface
{
    $item = $this->orderService->getItem($orderId, $itemId);
    return response()->json(['item' => $item]);
}

3.3 Controller Routing

ElliePHP supports two patterns for routing to controllers: single-method invokable controllers and array-based controller method routing.

Single-Method Controller Pattern

For simple controllers with a single action, pass the controller class directly:

php
Router::get('/', WelcomeController::class);

The controller must have a process() or __invoke() method:

php
<?php

namespace ElliePHP\Framework\Application\Http\Controllers;

use Psr\Http\Message\ResponseInterface;

final readonly class WelcomeController
{
    public function process(): ResponseInterface
    {
        return response()->json([
            'message' => 'Welcome to ElliePHP Microframework',
            'version' => '1.0.0'
        ]);
    }
}

Array-Based Controller Method Routing

For controllers with multiple actions, use array syntax [Controller::class, 'method']:

php
Router::get('/users', [UserController::class, 'index']);
Router::post('/users', [UserController::class, 'store']);
Router::get('/users/{id}', [UserController::class, 'show']);
Router::put('/users/{id}', [UserController::class, 'update']);
Router::delete('/users/{id}', [UserController::class, 'destroy']);

The controller class contains multiple methods:

php
<?php

namespace App\Http\Controllers;

use Psr\Http\Message\ResponseInterface;

final readonly class UserController
{
    public function __construct(
        private UserService $userService
    ) {}

    public function index(): ResponseInterface
    {
        $users = $this->userService->all();
        return response()->json(['users' => $users]);
    }

    public function store(): ResponseInterface
    {
        $data = request()->input();
        $user = $this->userService->create($data);
        return response()->json(['user' => $user], 201);
    }

    public function show(int $id): ResponseInterface
    {
        $user = $this->userService->find($id);
        return response()->json(['user' => $user]);
    }

    public function update(int $id): ResponseInterface
    {
        $data = request()->input();
        $user = $this->userService->update($id, $data);
        return response()->json(['user' => $user]);
    }

    public function destroy(int $id): ResponseInterface
    {
        $this->userService->delete($id);
        return response()->noContent();
    }
}

Controller Method Invocation

When a route matches, ElliePHP automatically:

  1. Resolves the controller from the dependency injection container
  2. Injects dependencies into the controller constructor
  3. Extracts route parameters from the URL
  4. Calls the specified method with parameters
  5. Returns the response

Example Flow:

php
// Route definition
Router::get('/posts/{id}', [PostController::class, 'show']);

// Request: GET /posts/42

// ElliePHP automatically:
// 1. Creates PostController instance with dependencies
// 2. Extracts $id = 42 from URL
// 3. Calls $controller->show(42)
// 4. Returns the response

WelcomeController Example

The default ElliePHP installation includes a WelcomeController that demonstrates the single-method pattern:

php
<?php

namespace ElliePHP\Framework\Application\Http\Controllers;

use ElliePHP\Framework\Kernel\HttpApplication;
use Psr\Http\Message\ResponseInterface;

final readonly class WelcomeController
{
    public function process(): ResponseInterface
    {
        return response()->json([
            'message' => 'Welcome to ElliePHP Microframework',
            'version' => HttpApplication::VERSION,
            'docs' => 'https://github.com/elliephp'
        ]);
    }
}

This controller is routed in routes/router.php:

php
Router::get('/', WelcomeController::class);

RESTful Controller Pattern

A common pattern is to create RESTful controllers with standard CRUD methods:

php
// routes/router.php
Router::get('/products', [ProductController::class, 'index']);      // List all
Router::post('/products', [ProductController::class, 'store']);     // Create
Router::get('/products/{id}', [ProductController::class, 'show']);  // Show one
Router::put('/products/{id}', [ProductController::class, 'update']); // Update
Router::delete('/products/{id}', [ProductController::class, 'destroy']); // Delete

3.4 Closure-Based Routes

For simple routes that don't require a full controller, you can use inline closures. This is useful for quick prototypes, simple responses, or routes that don't need complex business logic.

Basic Closure Routes

Define a route with an anonymous function:

php
Router::get('/test', static function () {
    return response()->json(['status' => 'ok']);
});

Closure Routes with Response Helpers

Closures can use all response helper methods:

JSON Response

php
Router::get('/api/status', static function () {
    return response()->json([
        'status' => 'operational',
        'timestamp' => time()
    ]);
});

XML Response

php
Router::get('/sitemap.xml', static function () {
    return response()->xml('<?xml version="1.0"?><urlset></urlset>');
});

HTML Response

php
Router::get('/hello', static function () {
    return response()->html('<h1>Hello, World!</h1>');
});

Plain Text Response

php
Router::get('/robots.txt', static function () {
    return response()->text("User-agent: *\nDisallow: /admin/");
});

Redirect Response

php
Router::get('/old-page', static function () {
    return response()->redirect('/new-page');
});

Closures with Route Parameters

Closures can accept route parameters just like controller methods:

php
Router::get('/greet/{name}', static function (string $name) {
    return response()->json([
        'message' => "Hello, {$name}!"
    ]);
});

Router::get('/users/{id}/status', static function (int $id) {
    return response()->json([
        'user_id' => $id,
        'status' => 'active'
    ]);
});

When to Use Closures vs Controllers

Use Closures When:

  • The route logic is very simple (1-3 lines)
  • You're prototyping or testing
  • The route returns static content
  • No business logic or dependencies are needed

Use Controllers When:

  • The route requires business logic
  • You need dependency injection
  • The logic might grow or change
  • You want to keep routes file clean and readable
  • You need to test the logic

Closure Examples

Health Check Endpoint

php
Router::get('/health', static function () {
    return response()->json([
        'status' => 'healthy',
        'timestamp' => date('c')
    ]);
});

API Version Info

php
Router::get('/api/version', static function () {
    return response()->json([
        'version' => '1.0.0',
        'api_version' => 'v1'
    ]);
});

Simple Redirect

php
Router::get('/docs', static function () {
    return response()->redirect('https://github.com/elliephp/docs');
});

Echo Parameter

php
Router::get('/echo/{message}', static function (string $message) {
    return response()->text($message);
});

Quick Test Route

php
Router::get('/test', static function () {
    return response()->xml('<?xml version="1.0"?><root></root>');
});

Closure Best Practices

  1. Keep closures simple - If logic exceeds 3-5 lines, use a controller
  2. Use static closures - Add static keyword for better performance
  3. Type-hint parameters - Always specify parameter types
  4. Use response helpers - Leverage response() helper for consistent responses
  5. Avoid business logic - Closures should not contain complex logic or database queries

3.5 Route Middleware

Middleware provides a way to filter or modify HTTP requests and responses. ElliePHP supports both global middleware (applied to all routes) and route-specific middleware.

Global Middleware

Global middleware is registered in configs/Middleware.php and runs on every request:

php
<?php

return [
    'global_middlewares' => [
        \ElliePHP\Framework\Application\Http\Middlewares\LoggingMiddleware::class,
        \ElliePHP\Framework\Application\Http\Middlewares\CorsMiddleware::class,
    ],
];

Execution Order:

Middleware executes in the order listed. In the example above:

  1. LoggingMiddleware runs first
  2. CorsMiddleware runs second
  3. Your route handler executes
  4. Response flows back through middleware in reverse order

Route-Specific Middleware

Apply middleware to specific routes using the third parameter:

php
Router::get('/admin/dashboard', [AdminController::class, 'dashboard'], [
    'middleware' => [AuthMiddleware::class]
]);

Router::post('/api/users', [UserController::class, 'store'], [
    'middleware' => [AuthMiddleware::class, RateLimitMiddleware::class]
]);

Middleware Examples

Authentication Middleware

php
Router::get('/profile', [ProfileController::class, 'show'], [
    'middleware' => [AuthMiddleware::class]
]);

Router::put('/profile', [ProfileController::class, 'update'], [
    'middleware' => [AuthMiddleware::class]
]);

Multiple Middleware

php
Router::post('/api/posts', [PostController::class, 'store'], [
    'middleware' => [
        AuthMiddleware::class,
        RateLimitMiddleware::class,
        ValidateJsonMiddleware::class
    ]
]);

Admin Routes with Middleware

php
Router::get('/admin/users', [AdminUserController::class, 'index'], [
    'middleware' => [AuthMiddleware::class, AdminMiddleware::class]
]);

Router::delete('/admin/users/{id}', [AdminUserController::class, 'destroy'], [
    'middleware' => [AuthMiddleware::class, AdminMiddleware::class]
]);

Global vs Route-Specific Middleware

Global Middleware:

  • Runs on every request
  • Defined in configs/Middleware.php
  • Good for: logging, CORS, security headers, request ID generation
  • Cannot be bypassed

Route-Specific Middleware:

  • Runs only on specified routes
  • Defined in route options array
  • Good for: authentication, authorization, rate limiting, validation
  • Applied per-route basis

Middleware Execution Flow

Request

Global Middleware 1 (LoggingMiddleware)

Global Middleware 2 (CorsMiddleware)

Route-Specific Middleware 1 (AuthMiddleware)

Route-Specific Middleware 2 (RateLimitMiddleware)

Route Handler (Controller)

Response

Route-Specific Middleware 2 (reverse)

Route-Specific Middleware 1 (reverse)

Global Middleware 2 (reverse)

Global Middleware 1 (reverse)

Client

Creating Custom Middleware

Middleware must implement the PSR-15 MiddlewareInterface:

php
<?php

namespace App\Http\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final readonly class CustomMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request, 
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Before route handler
        // Modify request, check conditions, etc.
        
        $response = $handler->handle($request);
        
        // After route handler
        // Modify response, add headers, etc.
        
        return $response;
    }
}

For more details on creating middleware, see Section 5: Middleware.

Middleware Best Practices

  1. Keep middleware focused - Each middleware should do one thing well
  2. Order matters - Place authentication before authorization
  3. Use global middleware sparingly - Only for truly universal concerns
  4. Consider performance - Middleware runs on every matching request
  5. Handle errors gracefully - Return appropriate error responses

3.6 Routes Command

ElliePHP includes a built-in routes command that displays all registered routes in your application. This is useful for debugging, documentation, and understanding your application's URL structure.

Basic Usage

Display all registered routes:

bash
php ellie routes

Command Output

The command displays a formatted table with route information:

Application Routes
┌────────┬─────────────────────────┬──────────────────────────┬──────┐
│ Method │ URI                     │ Handler                  │ Name │
├────────┼─────────────────────────┼──────────────────────────┼──────┤
│ GET    │ /                       │ WelcomeController@process│ -    │
│ GET    │ /users                  │ UserController@index     │ -    │
│ POST   │ /users                  │ UserController@store     │ -    │
│ GET    │ /users/{id}             │ UserController@show      │ -    │
│ PUT    │ /users/{id}             │ UserController@update    │ -    │
│ DELETE │ /users/{id}             │ UserController@destroy   │ -    │
│ GET    │ /test                   │ Closure                  │ -    │
└────────┴─────────────────────────┴──────────────────────────┴──────┘
Total routes: 7

Output Columns

Method

  • The HTTP method (GET, POST, PUT, DELETE, PATCH)
  • Indicates which HTTP verb the route responds to

URI

  • The URL pattern for the route
  • Shows dynamic parameters in curly braces (e.g., {id})

Handler

  • For controller methods: ControllerName@methodName
  • For single-method controllers: ControllerName@process
  • For closures: Closure

Name

  • Named routes (if specified)
  • Shows - if no name is assigned

Handler Format Examples

Array-Based Controller

php
Router::get('/users', [UserController::class, 'index']);
// Output: UserController@index

Single-Method Controller

php
Router::get('/', WelcomeController::class);
// Output: WelcomeController@process

Closure Route

php
Router::get('/test', static function () {
    return response()->json(['status' => 'ok']);
});
// Output: Closure

Viewing Routes During Development

The routes command is particularly useful during development:

After Adding New Routes

bash
php ellie routes
# Verify your new routes are registered correctly

Debugging Route Issues

bash
php ellie routes
# Check if route exists and has correct HTTP method

API Documentation

bash
php ellie routes > routes.txt
# Export route list for documentation

Route Listing Features

The command automatically:

  • Loads routes from routes/router.php
  • Extracts route method, URI, and handler information
  • Formats controller class names (shows short name)
  • Displays total route count
  • Handles empty route collections gracefully

Empty Routes Output

If no routes are registered:

bash
php ellie routes

Output:

Application Routes
No routes registered.

Use Cases

Development

  • Verify routes are registered correctly
  • Check route parameters and patterns
  • Understand application URL structure

Debugging

  • Confirm route exists before testing
  • Verify HTTP method matches expectations
  • Check handler is correctly specified

Documentation

  • Generate route list for API documentation
  • Share route structure with team members
  • Document available endpoints

Code Review

  • Review all application routes at once
  • Identify duplicate or conflicting routes
  • Ensure consistent naming patterns

Command Implementation

The routes command is implemented in app/Console/Command/RoutesCommand.php and automatically registered with the console application. It uses the Router facade to retrieve registered routes and formats them for display.

For more information on console commands, see Section 8: Console Commands.


4. Dependency Injection

ElliePHP uses PHP-DI, a powerful dependency injection container that implements the PSR-11 Container Interface standard. Dependency injection makes your code more testable, maintainable, and flexible by automatically managing class dependencies and promoting loose coupling between components.

4.1 Container Basics

The container is the heart of ElliePHP's dependency injection system. It automatically resolves class dependencies, manages object lifecycles, and provides a central registry for service bindings.

PHP-DI Integration

ElliePHP integrates PHP-DI seamlessly through the Container wrapper class. PHP-DI is a mature, feature-rich dependency injection container that provides:

  • Automatic Resolution: Automatically creates instances and resolves dependencies
  • Constructor Injection: Injects dependencies through class constructors
  • Interface Binding: Maps interfaces to concrete implementations
  • Factory Definitions: Creates complex objects using factory functions
  • Singleton Services: Manages single instances across the application
  • Lazy Loading: Defers object creation until actually needed

PHP-DI handles all the complexity of dependency resolution, allowing you to focus on writing clean, decoupled code.

PSR-11 Compliance

The container implements the PSR-11 Container Interface, which defines two standard methods:

get(string $id): mixed

  • Retrieves a service from the container
  • Throws NotFoundException if service doesn't exist
  • Throws ContainerException if service cannot be resolved

has(string $id): bool

  • Checks if a service is registered in the container
  • Returns true if service exists, false otherwise

PSR-11 compliance means ElliePHP's container is interoperable with any library or framework that supports the standard.

Singleton Pattern for Container

The Container class uses the singleton pattern to ensure a single container instance throughout your application:

php
use ElliePHP\Framework\Support\Container;

// Always returns the same instance
$container1 = Container::getInstance();
$container2 = Container::getInstance();

// $container1 === $container2 (true)

This singleton pattern ensures:

  • Consistent service resolution across the application
  • Efficient memory usage (one container instance)
  • Predictable behavior for singleton services
  • Thread-safe service access (in PHP's single-threaded model)

Container Class Methods

The Container class provides four primary methods for interacting with the dependency injection container:

getInstance(): ContainerInterface

Returns the singleton container instance. This is the foundation method that all other methods use internally.

php
use ElliePHP\Framework\Support\Container;

$container = Container::getInstance();

get(string $id): mixed

Resolves and returns a service from the container. If the service doesn't exist or cannot be resolved, it throws an exception.

php
use ElliePHP\Framework\Support\Container;
use App\Services\UserService;

// Resolve a service
$userService = Container::get(UserService::class);

// Use the service
$users = $userService->getAllUsers();

has(string $id): bool

Checks if a service is registered in the container without attempting to resolve it.

php
use ElliePHP\Framework\Support\Container;
use App\Services\UserService;

if (Container::has(UserService::class)) {
    $service = Container::get(UserService::class);
}

make(string $class, array $parameters = []): mixed

Creates a new instance of a class with dependencies resolved, optionally passing additional parameters.

php
use ElliePHP\Framework\Support\Container;
use App\Services\ReportService;

// Create instance with custom parameters
$reportService = Container::make(ReportService::class, [
    'format' => 'pdf',
    'includeCharts' => true
]);

The make() method is useful when you need multiple instances of a class or want to pass runtime parameters that aren't available at configuration time.

Container Usage Examples

Basic Service Resolution

php
use ElliePHP\Framework\Support\Container;
use App\Services\EmailService;

// Get the container
$container = Container::getInstance();

// Resolve a service
$emailService = $container->get(EmailService::class);

// Or use the static method
$emailService = Container::get(EmailService::class);

Checking Service Existence

php
use ElliePHP\Framework\Support\Container;

// Check before resolving
if (Container::has('database')) {
    $db = Container::get('database');
} else {
    // Handle missing service
    throw new RuntimeException('Database service not configured');
}

Creating Multiple Instances

php
use ElliePHP\Framework\Support\Container;
use App\Services\FileProcessor;

// Create multiple instances with different parameters
$processor1 = Container::make(FileProcessor::class, ['format' => 'json']);
$processor2 = Container::make(FileProcessor::class, ['format' => 'xml']);

// Each instance is independent
$processor1 !== $processor2; // true

Resolving with Dependencies

php
use ElliePHP\Framework\Support\Container;

// This class has dependencies
class OrderService
{
    public function __construct(
        private OrderRepository $repository,
        private EmailService $emailService,
        private CacheInterface $cache
    ) {}
}

// Container automatically resolves all dependencies
$orderService = Container::get(OrderService::class);
// OrderRepository, EmailService, and CacheInterface are automatically injected

4.2 Automatic Constructor Injection

One of the most powerful features of ElliePHP's dependency injection system is automatic constructor injection. The container automatically analyzes class constructors, resolves dependencies, and injects them when creating instances.

How Constructor Injection Works

When you request a class from the container, PHP-DI:

  1. Analyzes the constructor to identify required dependencies
  2. Resolves each dependency by looking up services in the container
  3. Recursively resolves dependencies of dependencies
  4. Creates the instance with all dependencies injected
  5. Returns the fully configured object

This happens automatically without any manual configuration or wiring.

Controller Examples with Injected Dependencies

Controllers are the most common place you'll use constructor injection. Dependencies are automatically injected when the router invokes your controller.

Simple Controller with Single Dependency

php
<?php

namespace ElliePHP\Framework\Application\Http\Controllers;

use ElliePHP\Framework\Application\Services\UserService;
use Psr\Http\Message\ResponseInterface;

final readonly class UserController
{
    public function __construct(
        private UserService $userService
    ) {}

    public function index(): ResponseInterface
    {
        $users = $this->userService->getAllUsers();
        
        return response()->json([
            'success' => true,
            'data' => $users
        ]);
    }

    public function show(int $id): ResponseInterface
    {
        $user = $this->userService->find($id);
        
        if (!$user) {
            return response()->notFound([
                'error' => 'User not found'
            ]);
        }
        
        return response()->json([
            'success' => true,
            'data' => $user
        ]);
    }
}

Controller with Multiple Dependencies

php
<?php

namespace ElliePHP\Framework\Application\Http\Controllers;

use ElliePHP\Framework\Application\Services\OrderService;
use ElliePHP\Framework\Application\Services\PaymentService;
use Psr\Http\Message\ResponseInterface;
use Psr\SimpleCache\CacheInterface;

final readonly class OrderController
{
    public function __construct(
        private OrderService $orderService,
        private PaymentService $paymentService,
        private CacheInterface $cache
    ) {}

    public function create(): ResponseInterface
    {
        $data = request()->input();
        
        // Create order
        $order = $this->orderService->create($data);
        
        // Process payment
        $payment = $this->paymentService->charge(
            $order->total,
            $data['payment_method']
        );
        
        // Cache the order
        $this->cache->set("order:{$order->id}", $order, 3600);
        
        return response()->created([
            'order' => $order,
            'payment' => $payment
        ]);
    }
}

Service Examples with Injected Dependencies

Services can also use constructor injection to receive their dependencies, creating a clean dependency chain.

Service with Repository Dependency

php
<?php

namespace ElliePHP\Framework\Application\Services;

use ElliePHP\Framework\Application\Repositories\UserRepository;

final readonly class UserService
{
    public function __construct(
        private UserRepository $repository
    ) {}

    public function getAllUsers(): array
    {
        return $this->repository->findAll();
    }

    public function find(int $id): ?array
    {
        return $this->repository->findById($id);
    }

    public function create(array $data): array
    {
        // Validate and sanitize data
        $validated = $this->validateUserData($data);
        
        // Create user through repository
        return $this->repository->create($validated);
    }

    private function validateUserData(array $data): array
    {
        // Validation logic
        return $data;
    }
}

Service with Multiple Dependencies

php
<?php

namespace ElliePHP\Framework\Application\Services;

use ElliePHP\Framework\Application\Repositories\UserRepository;
use Psr\SimpleCache\CacheInterface;
use Psr\Log\LoggerInterface;

final readonly class UserService
{
    public function __construct(
        private UserRepository $repository,
        private CacheInterface $cache,
        private LoggerInterface $logger
    ) {}

    public function find(int $id): ?array
    {
        // Try cache first
        $cacheKey = "user:{$id}";
        $cached = $this->cache->get($cacheKey);
        
        if ($cached !== null) {
            $this->logger->debug("User loaded from cache", ['id' => $id]);
            return $cached;
        }
        
        // Load from repository
        $user = $this->repository->findById($id);
        
        if ($user) {
            // Cache for 1 hour
            $this->cache->set($cacheKey, $user, 3600);
            $this->logger->info("User loaded from database", ['id' => $id]);
        }
        
        return $user;
    }

    public function create(array $data): array
    {
        $user = $this->repository->create($data);
        
        // Invalidate cache
        $this->cache->delete("users:all");
        
        $this->logger->info("User created", ['id' => $user['id']]);
        
        return $user;
    }
}

Middleware with Injected Dependencies

Middleware classes can also use constructor injection to receive services they need for request processing.

Authentication Middleware Example

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use ElliePHP\Framework\Application\Services\AuthService;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final readonly class AuthMiddleware implements MiddlewareInterface
{
    public function __construct(
        private AuthService $authService
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Extract token from request
        $token = $request->getHeaderLine('Authorization');
        
        // Validate token using injected service
        if (!$this->authService->validateToken($token)) {
            return response()->unauthorized([
                'error' => 'Invalid or missing authentication token'
            ]);
        }
        
        // Get user from token
        $user = $this->authService->getUserFromToken($token);
        
        // Add user to request attributes
        $request = $request->withAttribute('user', $user);
        
        // Continue to next middleware/controller
        return $handler->handle($request);
    }
}

Rate Limiting Middleware Example

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\SimpleCache\CacheInterface;

final readonly class RateLimitMiddleware implements MiddlewareInterface
{
    public function __construct(
        private CacheInterface $cache,
        private int $maxRequests = 60,
        private int $windowSeconds = 60
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $ip = $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown';
        $key = "rate_limit:{$ip}";
        
        // Get current request count
        $requests = (int) $this->cache->get($key, 0);
        
        if ($requests >= $this->maxRequests) {
            return response()->tooManyRequests([
                'error' => 'Rate limit exceeded',
                'retry_after' => $this->windowSeconds
            ]);
        }
        
        // Increment counter
        $this->cache->set($key, $requests + 1, $this->windowSeconds);
        
        return $handler->handle($request);
    }
}

Logging Middleware with Dependencies

The framework includes a LoggingMiddleware that demonstrates dependency injection in action:

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class LoggingMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $startTime = microtime(true);
        
        // Generate correlation ID
        $correlationId = $request->getHeaderLine('X-Correlation-ID')
            ?: bin2hex(random_bytes(8));
        
        $request = $request->withAttribute('correlation_id', $correlationId);
        
        // Process request
        $response = $handler->handle($request);
        
        // Log analytics
        $durationMs = round((microtime(true) - $startTime) * 1000, 2);
        
        report()->info('HTTP Request Analytics', [
            'correlation_id' => $correlationId,
            'method' => $request->getMethod(),
            'uri' => (string) $request->getUri(),
            'status' => $response->getStatusCode(),
            'duration_ms' => $durationMs,
        ]);
        
        return $response->withHeader('X-Correlation-ID', $correlationId);
    }
}

While this example doesn't show constructor injection, you could easily inject a logger service or cache service if needed.

Benefits of Constructor Injection

Explicit Dependencies

  • All dependencies are clearly declared in the constructor
  • Easy to see what a class needs to function
  • No hidden dependencies or magic

Testability

  • Easy to mock dependencies in unit tests
  • Can inject test doubles through constructor
  • No need for complex test setup

Immutability

  • Use readonly classes for immutable objects
  • Dependencies can't be changed after construction
  • Predictable behavior throughout object lifecycle

Type Safety

  • PHP 8.4 type declarations ensure correct types
  • IDE autocomplete and refactoring support
  • Compile-time type checking

No Manual Wiring

  • Container handles all dependency resolution
  • No need to manually instantiate dependencies
  • Reduces boilerplate code significantly

4.3 Service Binding

Service binding allows you to configure how the container resolves specific services. You can bind interfaces to implementations, define factory functions for complex object creation, and configure singleton services. All service bindings are defined in the configs/Container.php file.

The configs/Container.php File Structure

The container configuration file returns a PHP array where keys are service identifiers (usually class names or interface names) and values are binding definitions:

php
<?php

/**
 * Container Configuration
 *
 * Define your service bindings and dependencies here.
 * This file is loaded when the container is built.
 */

use Psr\Container\ContainerInterface;
use function DI\autowire;
use function DI\create;
use function DI\factory;

return [
    // Your service bindings go here
];

The file is automatically loaded when the container is built during application bootstrap.

Interface to Implementation Binding

One of the most common patterns is binding interfaces to concrete implementations. This allows you to program against interfaces while the container provides the actual implementation.

Basic Interface Binding

php
use App\Repositories\UserRepositoryInterface;
use App\Repositories\UserRepository;
use function DI\autowire;

return [
    // Bind interface to implementation
    UserRepositoryInterface::class => autowire(UserRepository::class),
];

Now when you type-hint the interface, the container provides the implementation:

php
final readonly class UserService
{
    public function __construct(
        private UserRepositoryInterface $repository  // Gets UserRepository
    ) {}
}

Multiple Interface Bindings

php
use function DI\autowire;

return [
    // Repository bindings
    UserRepositoryInterface::class => autowire(UserRepository::class),
    PostRepositoryInterface::class => autowire(PostRepository::class),
    CommentRepositoryInterface::class => autowire(CommentRepository::class),
    
    // Service bindings
    PaymentGatewayInterface::class => autowire(StripePaymentGateway::class),
    EmailServiceInterface::class => autowire(SendGridEmailService::class),
    StorageInterface::class => autowire(S3Storage::class),
];

Why Use Interface Binding?

  • Flexibility: Swap implementations without changing dependent code
  • Testability: Easy to provide mock implementations in tests
  • Decoupling: Depend on abstractions, not concrete classes
  • Multiple Implementations: Switch between different implementations based on configuration

Factory Definitions with Closures

For complex object creation that requires custom logic, use factory definitions. Factories are closures that receive the container and return the configured object.

Basic Factory Definition

php
use Psr\Container\ContainerInterface;
use function DI\factory;

return [
    'database' => function (ContainerInterface $c) {
        return new PDO(
            env('DB_DSN'),
            env('DB_USER'),
            env('DB_PASS'),
            [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            ]
        );
    },
];

Factory with Dependencies

php
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;

return [
    'mailer' => function (ContainerInterface $c) {
        $logger = $c->get(LoggerInterface::class);
        
        $mailer = new Mailer(
            host: env('MAIL_HOST'),
            port: env('MAIL_PORT'),
            username: env('MAIL_USERNAME'),
            password: env('MAIL_PASSWORD')
        );
        
        $mailer->setLogger($logger);
        
        return $mailer;
    },
];

Conditional Factory Logic

php
use Psr\Container\ContainerInterface;

return [
    StorageInterface::class => function (ContainerInterface $c) {
        $driver = env('STORAGE_DRIVER', 'local');
        
        return match ($driver) {
            's3' => new S3Storage(
                key: env('AWS_KEY'),
                secret: env('AWS_SECRET'),
                bucket: env('AWS_BUCKET')
            ),
            'local' => new LocalStorage(
                path: storage_path('uploads')
            ),
            default => throw new RuntimeException("Unknown storage driver: {$driver}")
        };
    },
];

Factory with Configuration Array

php
use Psr\Container\ContainerInterface;

return [
    'redis' => function (ContainerInterface $c) {
        $redis = new Redis();
        
        $redis->connect(
            env('REDIS_HOST', '127.0.0.1'),
            env('REDIS_PORT', 6379),
            env('REDIS_TIMEOUT', 5)
        );
        
        if ($password = env('REDIS_PASSWORD')) {
            $redis->auth($password);
        }
        
        $redis->select(env('REDIS_DATABASE', 0));
        
        return $redis;
    },
];

Singleton Services with Lazy Loading

Singleton services are created once and reused throughout the application. PHP-DI provides the lazy() method to defer creation until the service is first accessed.

Basic Singleton

php
use function DI\create;

return [
    CacheService::class => create(CacheService::class)->lazy(),
];

Singleton with Constructor Parameters

php
use function DI\create;

return [
    LogService::class => create(LogService::class)
        ->constructor(storage_logs_path('app.log'))
        ->lazy(),
];

Multiple Singletons

php
use function DI\create;

return [
    // These services are created once and reused
    CacheService::class => create(CacheService::class)->lazy(),
    SessionManager::class => create(SessionManager::class)->lazy(),
    ConfigRepository::class => create(ConfigRepository::class)->lazy(),
    EventDispatcher::class => create(EventDispatcher::class)->lazy(),
];

When to Use Singletons

Use singletons for:

  • Stateful Services: Services that maintain state across requests (cache, session)
  • Expensive Resources: Database connections, API clients
  • Shared Configuration: Configuration repositories, service registries
  • Event Systems: Event dispatchers, message buses

Don't use singletons for:

  • Stateless Services: Services with no internal state
  • Request-Scoped Data: Data that changes per request
  • Value Objects: Simple data containers

Complete Configuration Example

Here's a comprehensive example showing all binding types together:

php
<?php

use App\Repositories\UserRepositoryInterface;
use App\Repositories\UserRepository;
use App\Repositories\PostRepositoryInterface;
use App\Repositories\PostRepository;
use App\Services\CacheService;
use App\Services\EmailService;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use function DI\autowire;
use function DI\create;

return [
    // Interface to implementation bindings
    UserRepositoryInterface::class => autowire(UserRepository::class),
    PostRepositoryInterface::class => autowire(PostRepository::class),
    
    // Factory for database connection
    'database' => function (ContainerInterface $c) {
        return new PDO(
            env('DB_DSN'),
            env('DB_USER'),
            env('DB_PASS'),
            [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            ]
        );
    },
    
    // Factory for Redis connection
    'redis' => function (ContainerInterface $c) {
        $redis = new Redis();
        $redis->connect(
            env('REDIS_HOST', '127.0.0.1'),
            env('REDIS_PORT', 6379)
        );
        return $redis;
    },
    
    // Singleton services with lazy loading
    CacheService::class => create(CacheService::class)->lazy(),
    
    // Factory with dependencies
    EmailService::class => function (ContainerInterface $c) {
        $logger = $c->get(LoggerInterface::class);
        
        $service = new EmailService(
            host: env('MAIL_HOST'),
            port: env('MAIL_PORT'),
            username: env('MAIL_USERNAME'),
            password: env('MAIL_PASSWORD')
        );
        
        $service->setLogger($logger);
        
        return $service;
    },
    
    // Conditional binding based on environment
    StorageInterface::class => function (ContainerInterface $c) {
        return env('APP_ENV') === 'production'
            ? new S3Storage(env('AWS_KEY'), env('AWS_SECRET'))
            : new LocalStorage(storage_path('uploads'));
    },
];

4.4 Container Helper Function

ElliePHP provides a convenient container() helper function for accessing the dependency injection container and resolving services from anywhere in your application.

Getting the Container Instance

Call container() without arguments to get the container instance:

php
// Get the container
$container = container();

// Now you can use PSR-11 methods
$service = $container->get(UserService::class);
$exists = $container->has(UserService::class);

This is equivalent to:

php
use ElliePHP\Framework\Support\Container;

$container = Container::getInstance();

Resolving Services

Pass a service identifier to container() to resolve it directly:

php
// Resolve a service
$userService = container(UserService::class);

// Use the service
$users = $userService->getAllUsers();

This is a shorthand for:

php
$userService = container()->get(UserService::class);

Practical Examples

In Controllers

While controllers use constructor injection, you might occasionally need to resolve services dynamically:

php
final readonly class AdminController
{
    public function stats(): ResponseInterface
    {
        // Dynamically resolve services based on request
        $reportType = request()->input('type', 'users');
        
        $service = match ($reportType) {
            'users' => container(UserReportService::class),
            'orders' => container(OrderReportService::class),
            'revenue' => container(RevenueReportService::class),
            default => throw new InvalidArgumentException('Invalid report type')
        };
        
        $data = $service->generate();
        
        return response()->json(['report' => $data]);
    }
}

In Helper Functions

The container() function is particularly useful in global helper functions:

php
/**
 * Get the current authenticated user
 */
function auth(): ?User
{
    $authService = container(AuthService::class);
    return $authService->user();
}

/**
 * Dispatch an event
 */
function event(string $eventName, array $data = []): void
{
    $dispatcher = container(EventDispatcher::class);
    $dispatcher->dispatch($eventName, $data);
}

/**
 * Get a configuration value
 */
function setting(string $key, mixed $default = null): mixed
{
    $settings = container(SettingsRepository::class);
    return $settings->get($key, $default);
}

In Service Classes

While constructor injection is preferred, sometimes you need dynamic resolution:

php
final readonly class PluginManager
{
    public function __construct(
        private ContainerInterface $container
    ) {}

    public function loadPlugin(string $pluginClass): Plugin
    {
        // Dynamically resolve plugin with dependencies
        return $this->container->get($pluginClass);
    }
}

// Or using the helper
final class PluginManager
{
    public function loadPlugin(string $pluginClass): Plugin
    {
        return container($pluginClass);
    }
}

In Closures and Callbacks

php
// Route with closure
Router::get('/status', function () {
    $cache = container(CacheInterface::class);
    $health = container(HealthCheckService::class);
    
    return response()->json([
        'status' => 'ok',
        'cache' => $cache->has('health') ? 'connected' : 'disconnected',
        'services' => $health->check()
    ]);
});

In Console Commands

php
use ElliePHP\Components\Console\BaseCommand;

final class SyncCommand extends BaseCommand
{
    protected function configure(): void
    {
        $this->setName('sync:users')
             ->setDescription('Sync users from external API');
    }

    protected function handle(): int
    {
        // Resolve services in command
        $apiClient = container(ApiClient::class);
        $userService = container(UserService::class);
        
        $users = $apiClient->fetchUsers();
        
        foreach ($users as $userData) {
            $userService->createOrUpdate($userData);
            $this->info("Synced user: {$userData['email']}");
        }
        
        return self::SUCCESS;
    }
}

When to Use the Container Helper

Use container() when:

  • Writing global helper functions
  • Dynamically resolving services based on runtime conditions
  • Working in contexts where constructor injection isn't available (closures, callbacks)
  • Resolving optional dependencies conditionally

Prefer constructor injection when:

  • Dependencies are known at class definition time
  • Working in controllers, services, or middleware
  • Dependencies are required for the class to function
  • You want explicit, testable dependencies

Container Helper vs Constructor Injection

Constructor Injection (Preferred)

php
// ✅ Explicit, testable, clear dependencies
final readonly class UserService
{
    public function __construct(
        private UserRepository $repository,
        private CacheInterface $cache
    ) {}
}

Container Helper (When Needed)

php
// ✅ Useful for dynamic resolution
function currentUser(): ?User
{
    return container(AuthService::class)->user();
}

// ❌ Avoid in regular classes - use constructor injection instead
final class UserService
{
    public function getAll(): array
    {
        // Don't do this - use constructor injection
        $repository = container(UserRepository::class);
        return $repository->findAll();
    }
}

4.5 Production Optimization

ElliePHP's dependency injection container includes powerful optimization features for production environments. These optimizations significantly improve performance by eliminating runtime reflection and generating optimized PHP code.

Container Compilation

Container compilation is the process of analyzing your service definitions and generating optimized PHP code that eliminates the need for runtime reflection and configuration parsing.

How Compilation Works:

  1. Analysis Phase: PHP-DI analyzes all service definitions and dependencies
  2. Code Generation: Generates optimized PHP code for service resolution
  3. File Writing: Writes compiled container to storage/Cache/CompiledContainer.php
  4. Fast Loading: Subsequent requests load the pre-compiled container

Performance Benefits:

  • 40-60% faster container operations
  • Eliminates reflection overhead for dependency resolution
  • Reduces memory usage by avoiding runtime analysis
  • Faster application bootstrap time

APP_ENV=production Configuration

Enable container compilation by setting the APP_ENV environment variable to production:

.env File

env
# Enable production optimizations
APP_ENV=production

# Other production settings
APP_DEBUG=false
CACHE_DRIVER=redis

When APP_ENV=production, the container automatically:

  • Enables compilation
  • Writes compiled container to cache directory
  • Generates proxy classes for lazy loading
  • Disables development-only features

Container Build Configuration

The Container class automatically configures compilation based on environment:

php
// From src/Support/Container.php
private static function build(): ContainerInterface
{
    $builder = new ContainerBuilder();

    // Enable compilation for production
    if (env('APP_ENV') === 'production') {
        $builder->enableCompilation(storage_cache_path());
        $builder->writeProxiesToFile(true, storage_cache_path() . '/proxies');
    }

    // Load container definitions
    $definitions = [];
    $configFile = root_path('configs/Container.php');

    if (file_exists($configFile)) {
        $definitions = require $configFile;
    }

    $builder->addDefinitions($definitions);

    return $builder->build();
}

Proxy Generation

Proxy generation creates lightweight proxy classes for lazy-loaded services. Proxies defer object creation until the service is actually used.

How Proxies Work:

  1. Proxy Creation: PHP-DI generates a proxy class that extends your service
  2. Lazy Instantiation: The real service is only created when a method is called
  3. Transparent Usage: Proxies are transparent - your code doesn't know the difference
  4. Memory Savings: Services that aren't used are never instantiated

Example:

php
// Container configuration
return [
    ExpensiveService::class => create(ExpensiveService::class)->lazy(),
];

// First access - proxy is returned immediately (fast)
$service = container(ExpensiveService::class);

// Real service is created only when you call a method
$result = $service->doSomething(); // Now the real service is instantiated

Proxy File Location:

Proxies are written to storage/Cache/proxies/ when APP_ENV=production:

storage/Cache/proxies/
├── ProxyExpensiveService.php
├── ProxyCacheService.php
└── ProxySessionManager.php

Cache File Locations

The container system uses the storage/Cache/ directory for all compiled and generated files:

Directory Structure:

storage/Cache/
├── CompiledContainer.php          # Compiled container code
├── proxies/                        # Generated proxy classes
│   ├── ProxyServiceName.php
│   └── ...
├── cache.db                        # SQLite cache (if using SQLite driver)
└── ellie_routes_*.cache           # Route cache files

CompiledContainer.php

This file contains the optimized container code. It's automatically generated when APP_ENV=production and regenerated when:

  • Container configuration changes
  • Service definitions are modified
  • Cache is cleared manually

Proxies Directory

Contains generated proxy classes for lazy-loaded services. Each lazy service gets its own proxy class file.

File Permissions

Ensure the cache directory is writable:

bash
chmod -R 775 storage/Cache

Clearing Compiled Container

When you modify service bindings or container configuration, you need to clear the compiled container:

Using the Cache Clear Command:

bash
# Clear all caches including container
php ellie cache:clear --all

# Clear only container cache
php ellie cache:clear --config

Manual Deletion:

bash
# Remove compiled container
rm storage/Cache/CompiledContainer.php

# Remove proxy classes
rm -rf storage/Cache/proxies/*

Automatic Regeneration:

After clearing, the container will automatically recompile on the next request when APP_ENV=production.

Development vs Production

Development Mode (APP_ENV=development or not set):

  • ✅ No compilation - changes take effect immediately
  • ✅ Easier debugging with full stack traces
  • ✅ No need to clear cache after changes
  • ❌ Slower performance due to runtime reflection
  • ❌ Higher memory usage

Production Mode (APP_ENV=production):

  • ✅ Compiled container for maximum performance
  • ✅ Generated proxies for lazy loading
  • ✅ 40-60% faster container operations
  • ✅ Lower memory footprint
  • ❌ Requires cache clearing after configuration changes
  • ❌ Less detailed error messages

Production Deployment Checklist

When deploying to production, follow these steps:

1. Set Environment Variables

env
APP_ENV=production
APP_DEBUG=false

2. Clear Existing Cache

bash
php ellie cache:clear --all

3. Warm Up Cache (Optional)

bash
# Make a request to generate compiled container
curl https://your-app.com/

4. Verify Compilation

bash
# Check that compiled container exists
ls -la storage/Cache/CompiledContainer.php

# Check proxy generation
ls -la storage/Cache/proxies/

5. Set Proper Permissions

bash
chmod -R 775 storage/Cache
chown -R www-data:www-data storage/Cache

Performance Benchmarks

Typical performance improvements with production optimizations:

Container Resolution:

  • Development: ~2-3ms per service resolution
  • Production: ~0.5-1ms per service resolution
  • Improvement: 50-70% faster

Application Bootstrap:

  • Development: ~15-20ms
  • Production: ~5-8ms
  • Improvement: 60-70% faster

Memory Usage:

  • Development: ~4-6MB base memory
  • Production: ~2-3MB base memory
  • Improvement: 40-50% less memory

These improvements compound with application complexity - larger applications see even greater benefits from compilation.

Troubleshooting Production Issues

Container Not Compiling

Check that:

  • APP_ENV=production is set in .env
  • storage/Cache/ directory is writable
  • No syntax errors in configs/Container.php

Services Not Resolving

  • Clear the compiled container: php ellie cache:clear --config
  • Check service definitions in configs/Container.php
  • Verify class names and namespaces are correct

Proxy Errors

  • Ensure storage/Cache/proxies/ is writable
  • Clear proxy cache: rm -rf storage/Cache/proxies/*
  • Check that lazy services are properly configured

Performance Not Improving

  • Verify APP_ENV=production is actually set
  • Check that CompiledContainer.php exists and is recent
  • Ensure OPcache is enabled in PHP
  • Consider using Redis or APCu for application caching

5. Middleware

5.1 PSR-15 Middleware Interface

ElliePHP implements the PSR-15 HTTP Server Request Handlers standard for middleware. This standard defines a consistent interface for processing HTTP requests and responses through a middleware pipeline.

Understanding PSR-15

PSR-15 defines two key interfaces:

MiddlewareInterface

  • Processes an incoming server request
  • Can modify the request before passing it to the next handler
  • Can modify the response after the handler returns
  • Must call the request handler to continue the pipeline

RequestHandlerInterface

  • Handles a server request and produces a response
  • Represents the next middleware in the chain or the final application handler

The MiddlewareInterface Contract

All middleware in ElliePHP must implement the Psr\Http\Server\MiddlewareInterface:

php
namespace Psr\Http\Server;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

interface MiddlewareInterface
{
    /**
     * Process an incoming server request.
     *
     * @param ServerRequestInterface $request The request
     * @param RequestHandlerInterface $handler The handler
     * @return ResponseInterface The response
     */
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface;
}

The process() Method Signature

The process() method is the heart of middleware functionality:

Parameters:

  • ServerRequestInterface $request: The incoming HTTP request (PSR-7)

    • Contains all request data: URI, method, headers, body, query parameters
    • Immutable - modifications create new request instances
    • Can carry attributes for passing data between middleware
  • RequestHandlerInterface $handler: The next handler in the pipeline

    • Represents either the next middleware or the final controller
    • Call $handler->handle($request) to continue the pipeline
    • Returns a ResponseInterface

Return Value:

  • ResponseInterface: The HTTP response (PSR-7)
    • Must return a valid PSR-7 response
    • Can be the response from the handler (passed through)
    • Can be a modified version of the handler's response
    • Can be a completely new response (short-circuit)

Request Handler Pattern

The request handler pattern is central to how middleware works in ElliePHP:

1. Receive Request and Handler

Middleware receives the current request and a handler representing the rest of the pipeline:

php
public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
): ResponseInterface {
    // Your middleware logic here
}

2. Process Before Handler (Optional)

Perform operations before the request reaches the controller:

php
public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
): ResponseInterface {
    // Validate authentication
    if (!$this->isAuthenticated($request)) {
        return response()->unauthorized(['error' => 'Not authenticated']);
    }
    
    // Add data to request
    $request = $request->withAttribute('user_id', $this->getUserId($request));
    
    // Continue to next handler
    return $handler->handle($request);
}

3. Delegate to Handler

Call $handler->handle($request) to pass control to the next middleware or controller:

php
// Continue the pipeline
$response = $handler->handle($request);

Important: You must call $handler->handle() to continue the pipeline, unless you're intentionally short-circuiting (e.g., returning an error response).

4. Process After Handler (Optional)

Perform operations after the controller returns a response:

php
public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
): ResponseInterface {
    // Get response from handler
    $response = $handler->handle($request);
    
    // Add headers to response
    return $response
        ->withHeader('X-Custom-Header', 'value')
        ->withHeader('X-Response-Time', $this->calculateTime());
}

5. Return Response

Always return a ResponseInterface:

php
return $response;

Complete Middleware Flow Example

Here's a complete example showing the full request/response flow:

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class TimingMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // 1. Before handler - record start time
        $startTime = microtime(true);
        
        // 2. Delegate to handler (next middleware or controller)
        $response = $handler->handle($request);
        
        // 3. After handler - calculate duration
        $duration = microtime(true) - $startTime;
        
        // 4. Modify response - add timing header
        return $response->withHeader('X-Response-Time', round($duration * 1000, 2) . 'ms');
    }
}

Flow Visualization:

Request → Middleware::process()
            ↓ (before handler)
            ↓ $handler->handle($request)

          Next Middleware or Controller

          Response
            ↓ (after handler)
            ↓ modify response
          Return Response

Short-Circuiting the Pipeline

Middleware can return a response without calling the handler, effectively short-circuiting the pipeline:

php
public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
): ResponseInterface {
    // Check if request should be blocked
    if ($this->isBlocked($request)) {
        // Return response immediately - handler is never called
        return response()->forbidden(['error' => 'Access denied']);
    }
    
    // Continue normally
    return $handler->handle($request);
}

When to Short-Circuit:

  • Authentication failures
  • Rate limit exceeded
  • Invalid request format
  • Maintenance mode
  • IP blocking

Modifying Requests

Since PSR-7 requests are immutable, modifications create new instances:

php
public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
): ResponseInterface {
    // Add attribute to request
    $request = $request->withAttribute('correlation_id', $this->generateId());
    
    // Add header to request
    $request = $request->withHeader('X-Internal-Request', 'true');
    
    // Pass modified request to handler
    return $handler->handle($request);
}

Modifying Responses

Similarly, responses are immutable:

php
public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
): ResponseInterface {
    $response = $handler->handle($request);
    
    // Add headers
    return $response
        ->withHeader('X-Frame-Options', 'DENY')
        ->withHeader('X-Content-Type-Options', 'nosniff')
        ->withHeader('X-XSS-Protection', '1; mode=block');
}

PSR-15 Benefits

Standardization

  • Consistent interface across all middleware
  • Interoperable with other PSR-15 compatible frameworks
  • Clear contract for middleware behavior

Composability

  • Middleware can be easily combined and reordered
  • Each middleware has a single responsibility
  • Pipeline pattern enables flexible request processing

Testability

  • Easy to test middleware in isolation
  • Mock request and handler for unit tests
  • Predictable input/output behavior

Type Safety

  • Strong typing with PSR-7 and PSR-15 interfaces
  • IDE autocomplete and type checking
  • Compile-time error detection

5.2 Creating Custom Middleware

Creating custom middleware in ElliePHP is straightforward. Follow these steps to build middleware that processes requests and responses according to your application's needs.

Step-by-Step Middleware Creation Guide

Step 1: Create the Middleware Class File

Create a new PHP file in the app/Http/Middlewares/ directory. Use a descriptive name that reflects the middleware's purpose:

bash
# Example: Create an authentication middleware
touch app/Http/Middlewares/AuthMiddleware.php

Step 2: Define the Class Structure

Start with the basic class structure implementing MiddlewareInterface:

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class YourMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Your middleware logic here
        
        return $handler->handle($request);
    }
}

Step 3: Implement the process() Method

Add your custom logic to the process() method. This is where you'll handle request processing, validation, modification, or any other middleware functionality.

Step 4: Handle Request and Response

Decide whether your middleware needs to:

  • Process before the handler (request modification, validation)
  • Process after the handler (response modification, logging)
  • Both (timing, analytics)

Step 5: Return the Response

Always return a ResponseInterface from the process() method.

Complete Middleware Class Example

Here's a complete, production-ready authentication middleware example:

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
 * Authentication Middleware
 * 
 * Validates that requests contain a valid authentication token.
 * Blocks unauthorized requests and adds user information to the request.
 */
final class AuthMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Extract authorization header
        $authHeader = $request->getHeaderLine('Authorization');
        
        // Check if token exists
        if (empty($authHeader)) {
            return response()->json([
                'error' => 'Missing authorization token'
            ], 401);
        }
        
        // Extract token from "Bearer <token>" format
        if (!str_starts_with($authHeader, 'Bearer ')) {
            return response()->json([
                'error' => 'Invalid authorization format'
            ], 401);
        }
        
        $token = substr($authHeader, 7);
        
        // Validate token (simplified example)
        $userId = $this->validateToken($token);
        
        if ($userId === null) {
            return response()->json([
                'error' => 'Invalid or expired token'
            ], 401);
        }
        
        // Add user ID to request attributes for use in controllers
        $request = $request->withAttribute('user_id', $userId);
        $request = $request->withAttribute('authenticated', true);
        
        // Continue to next handler
        return $handler->handle($request);
    }
    
    /**
     * Validate authentication token
     * 
     * @param string $token The authentication token
     * @return int|null User ID if valid, null otherwise
     */
    private function validateToken(string $token): ?int
    {
        // In a real application, validate against database or cache
        // This is a simplified example
        
        if (strlen($token) < 32) {
            return null;
        }
        
        // Example: decode JWT, check database, verify signature, etc.
        // For this example, we'll just return a mock user ID
        return 1;
    }
}

Request/Response Manipulation

Manipulating Requests

Requests are immutable, so modifications create new instances:

php
public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
): ResponseInterface {
    // Add custom attributes
    $request = $request->withAttribute('request_id', uniqid());
    $request = $request->withAttribute('timestamp', time());
    
    // Add or modify headers
    $request = $request->withHeader('X-Internal-Request', 'true');
    
    // Modify query parameters (create new URI)
    $uri = $request->getUri();
    $query = $request->getQueryParams();
    $query['processed'] = 'true';
    
    // Note: Modifying query params requires creating a new request
    // with the modified URI
    
    return $handler->handle($request);
}

Manipulating Responses

Responses are also immutable:

php
public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
): ResponseInterface {
    // Get response from handler
    $response = $handler->handle($request);
    
    // Add security headers
    $response = $response
        ->withHeader('X-Frame-Options', 'DENY')
        ->withHeader('X-Content-Type-Options', 'nosniff')
        ->withHeader('Strict-Transport-Security', 'max-age=31536000');
    
    // Modify status code if needed
    if ($response->getStatusCode() === 200) {
        // Could change based on some condition
    }
    
    // Add custom header with request data
    $requestId = $request->getAttribute('request_id');
    $response = $response->withHeader('X-Request-ID', $requestId);
    
    return $response;
}

Modifying Response Body

php
public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
): ResponseInterface {
    $response = $handler->handle($request);
    
    // Read existing body
    $body = (string) $response->getBody();
    
    // Modify content (example: add wrapper)
    $modified = json_encode([
        'success' => true,
        'data' => json_decode($body, true),
        'timestamp' => time()
    ]);
    
    // Create new response with modified body
    return $response
        ->withBody(stream_for($modified))
        ->withHeader('Content-Length', strlen($modified));
}

Returning Response from Handler

The most common pattern is to call the handler and return its response:

Simple Pass-Through

php
public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
): ResponseInterface {
    // Do something before
    $this->logRequest($request);
    
    // Get response from handler
    $response = $handler->handle($request);
    
    // Do something after
    $this->logResponse($response);
    
    // Return the response
    return $response;
}

Conditional Processing

php
public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
): ResponseInterface {
    // Check condition
    if ($this->shouldProcess($request)) {
        // Process and modify
        $request = $this->processRequest($request);
        $response = $handler->handle($request);
        return $this->processResponse($response);
    }
    
    // Pass through unchanged
    return $handler->handle($request);
}

Early Return (Short-Circuit)

php
public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
): ResponseInterface {
    // Check if request should be blocked
    if ($this->isRateLimited($request)) {
        // Return response without calling handler
        return response()->json([
            'error' => 'Rate limit exceeded'
        ], 429);
    }
    
    // Continue normally
    return $handler->handle($request);
}

Common Middleware Patterns

Validation Middleware

php
final class JsonValidationMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Only validate POST/PUT/PATCH requests
        if (in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'])) {
            $contentType = $request->getHeaderLine('Content-Type');
            
            if (!str_contains($contentType, 'application/json')) {
                return response()->json([
                    'error' => 'Content-Type must be application/json'
                ], 400);
            }
            
            // Validate JSON body
            $body = (string) $request->getBody();
            json_decode($body);
            
            if (json_last_error() !== JSON_ERROR_NONE) {
                return response()->json([
                    'error' => 'Invalid JSON: ' . json_last_error_msg()
                ], 400);
            }
        }
        
        return $handler->handle($request);
    }
}

Request Transformation Middleware

php
final class RequestTransformMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Parse JSON body and add as parsed body
        if ($request->getHeaderLine('Content-Type') === 'application/json') {
            $body = (string) $request->getBody();
            $data = json_decode($body, true);
            
            if ($data !== null) {
                $request = $request->withParsedBody($data);
            }
        }
        
        // Normalize headers
        $request = $request->withHeader('X-Processed', 'true');
        
        return $handler->handle($request);
    }
}

Response Transformation Middleware

php
final class ResponseWrapperMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $response = $handler->handle($request);
        
        // Only wrap JSON responses
        if (str_contains($response->getHeaderLine('Content-Type'), 'application/json')) {
            $body = (string) $response->getBody();
            $data = json_decode($body, true);
            
            $wrapped = [
                'status' => $response->getStatusCode(),
                'data' => $data,
                'timestamp' => date('c')
            ];
            
            $newBody = json_encode($wrapped);
            
            return $response
                ->withBody(stream_for($newBody))
                ->withHeader('Content-Length', strlen($newBody));
        }
        
        return $response;
    }
}

Best Practices

Keep Middleware Focused

  • Each middleware should have a single responsibility
  • Don't combine unrelated functionality
  • Small, focused middleware is easier to test and maintain

Use Readonly Classes When Possible

  • If middleware has no mutable state, make it readonly
  • Promotes immutability and thread safety

Handle Errors Gracefully

  • Always return a valid response
  • Provide clear error messages
  • Use appropriate HTTP status codes

Document Your Middleware

  • Add class-level docblocks explaining purpose
  • Document any configuration or dependencies
  • Explain side effects or modifications

Consider Performance

  • Middleware runs on every request
  • Avoid expensive operations when possible
  • Use caching for repeated lookups

Test Thoroughly

  • Unit test middleware in isolation
  • Test both success and failure paths
  • Verify request/response modifications

5.3 Middleware Registration

Once you've created custom middleware, you need to register it so ElliePHP knows to execute it. Middleware registration is configured in the configs/Middleware.php file.

The configs/Middleware.php Structure

The middleware configuration file returns a PHP array with a global_middlewares key that contains an array of middleware class names:

php
<?php

/**
 * Middleware Configuration
 *
 * Define global middleware that runs on every request.
 * Middleware is executed in the order listed.
 *
 * Add your custom middleware classes to the array below.
 */

return [
    'global_middlewares' => [
        // Your middleware classes here
    ],
];

This file is located at configs/Middleware.php in your project root.

The global_middlewares Array

The global_middlewares array defines middleware that runs on every HTTP request. Each entry is a fully-qualified class name (FQCN) of a middleware class.

Basic Structure:

php
return [
    'global_middlewares' => [
        \ElliePHP\Framework\Application\Http\Middlewares\LoggingMiddleware::class,
        \ElliePHP\Framework\Application\Http\Middlewares\CorsMiddleware::class,
    ],
];

Key Points:

  • Each middleware must be a fully-qualified class name
  • Use the ::class constant for type safety and IDE support
  • Middleware classes must implement Psr\Http\Server\MiddlewareInterface
  • The array can contain any number of middleware

Middleware Execution Order

Middleware executes in the order it appears in the global_middlewares array. This order is critical because:

  1. Request Processing: Middleware processes requests from top to bottom
  2. Response Processing: Middleware processes responses from bottom to top
  3. Short-Circuiting: Earlier middleware can block later middleware from executing

Execution Flow Visualization:

php
return [
    'global_middlewares' => [
        LoggingMiddleware::class,      // 1st: Processes request first
        CorsMiddleware::class,          // 2nd: Processes request second
        AuthMiddleware::class,          // 3rd: Processes request third
        RateLimitMiddleware::class,     // 4th: Processes request fourth
    ],
];

Request Flow (Top to Bottom):

Request

LoggingMiddleware (start timing)

CorsMiddleware (add CORS headers)

AuthMiddleware (validate token)

RateLimitMiddleware (check rate limit)

Controller (handle request)

Response Flow (Bottom to Top):

Controller (return response)

RateLimitMiddleware (update rate limit counter)

AuthMiddleware (add auth headers)

CorsMiddleware (finalize CORS headers)

LoggingMiddleware (log duration, add timing header)

Response

Order Matters: Practical Examples

Example 1: Logging Should Be First

Place logging middleware first to capture the entire request/response cycle:

php
return [
    'global_middlewares' => [
        LoggingMiddleware::class,      // ✓ Captures everything
        AuthMiddleware::class,
        RateLimitMiddleware::class,
    ],
];

If logging is last, it won't capture requests blocked by earlier middleware:

php
return [
    'global_middlewares' => [
        AuthMiddleware::class,         // Blocks unauthorized requests
        RateLimitMiddleware::class,
        LoggingMiddleware::class,      // ✗ Never sees blocked requests
    ],
];

Example 2: Authentication Before Authorization

Validate identity before checking permissions:

php
return [
    'global_middlewares' => [
        AuthMiddleware::class,         // ✓ First: Verify user identity
        AuthorizationMiddleware::class, // ✓ Second: Check permissions
        // ...
    ],
];

Example 3: CORS Should Be Early

CORS middleware should run early to handle preflight requests:

php
return [
    'global_middlewares' => [
        CorsMiddleware::class,         // ✓ Handle OPTIONS requests early
        AuthMiddleware::class,         // Auth runs after CORS
        // ...
    ],
];

Example 4: Rate Limiting Placement

Place rate limiting after authentication to rate-limit per user:

php
return [
    'global_middlewares' => [
        AuthMiddleware::class,         // Identify user
        RateLimitMiddleware::class,    // Rate limit by user ID
        // ...
    ],
];

Or place it first to rate-limit by IP regardless of authentication:

php
return [
    'global_middlewares' => [
        RateLimitMiddleware::class,    // Rate limit by IP
        AuthMiddleware::class,         // Then authenticate
        // ...
    ],
];

Adding Custom Middleware

To add your custom middleware to the global pipeline:

Step 1: Create Your Middleware Class

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class SecurityHeadersMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $response = $handler->handle($request);
        
        return $response
            ->withHeader('X-Frame-Options', 'DENY')
            ->withHeader('X-Content-Type-Options', 'nosniff')
            ->withHeader('X-XSS-Protection', '1; mode=block');
    }
}

Step 2: Register in configs/Middleware.php

php
<?php

return [
    'global_middlewares' => [
        \ElliePHP\Framework\Application\Http\Middlewares\LoggingMiddleware::class,
        \ElliePHP\Framework\Application\Http\Middlewares\CorsMiddleware::class,
        \ElliePHP\Framework\Application\Http\Middlewares\SecurityHeadersMiddleware::class,
    ],
];

Step 3: Test Your Middleware

Start the development server and make a request:

bash
php ellie serve
bash
curl -I http://127.0.0.1:8000/

You should see your custom headers in the response.

Complete Configuration Example

Here's a complete, production-ready middleware configuration:

php
<?php

/**
 * Middleware Configuration
 *
 * Define global middleware that runs on every request.
 * Middleware is executed in the order listed.
 */

return [
    'global_middlewares' => [
        // 1. Logging - Track all requests (should be first)
        \ElliePHP\Framework\Application\Http\Middlewares\LoggingMiddleware::class,
        
        // 2. CORS - Handle cross-origin requests early
        \ElliePHP\Framework\Application\Http\Middlewares\CorsMiddleware::class,
        
        // 3. Security Headers - Add security headers to all responses
        \ElliePHP\Framework\Application\Http\Middlewares\SecurityHeadersMiddleware::class,
        
        // 4. Rate Limiting - Prevent abuse (before auth to limit by IP)
        \ElliePHP\Framework\Application\Http\Middlewares\RateLimitMiddleware::class,
        
        // 5. Authentication - Verify user identity
        \ElliePHP\Framework\Application\Http\Middlewares\AuthMiddleware::class,
        
        // 6. Request Validation - Validate request format
        \ElliePHP\Framework\Application\Http\Middlewares\JsonValidationMiddleware::class,
    ],
];

Conditional Middleware Registration

You can conditionally register middleware based on environment or configuration:

php
<?php

$middlewares = [
    \ElliePHP\Framework\Application\Http\Middlewares\LoggingMiddleware::class,
    \ElliePHP\Framework\Application\Http\Middlewares\CorsMiddleware::class,
];

// Add debug middleware only in development
if (env('APP_DEBUG', false)) {
    $middlewares[] = \ElliePHP\Framework\Application\Http\Middlewares\DebugMiddleware::class;
}

// Add authentication only if enabled
if (env('AUTH_ENABLED', true)) {
    $middlewares[] = \ElliePHP\Framework\Application\Http\Middlewares\AuthMiddleware::class;
}

return [
    'global_middlewares' => $middlewares,
];

Removing Default Middleware

If you want to remove the default middleware, simply clear the array:

php
<?php

return [
    'global_middlewares' => [
        // Empty array - no middleware will run
    ],
];

Or replace with your own:

php
<?php

return [
    'global_middlewares' => [
        \App\Middlewares\CustomLoggingMiddleware::class,
        \App\Middlewares\CustomCorsMiddleware::class,
    ],
];

Middleware Registration Best Practices

Order Strategically

  • Place logging first to capture all requests
  • Place CORS early to handle preflight requests
  • Place authentication before authorization
  • Place rate limiting based on your strategy (IP vs user)

Keep It Minimal

  • Only register middleware that needs to run on every request
  • Consider route-specific middleware for specialized needs
  • Too much global middleware slows down all requests

Document Your Choices

  • Add comments explaining why middleware is in a specific order
  • Document any dependencies between middleware
  • Note any environment-specific middleware

Test Thoroughly

  • Test middleware order with various request scenarios
  • Verify short-circuiting works as expected
  • Check that response modifications don't conflict

Monitor Performance

  • Each middleware adds overhead to every request
  • Profile middleware execution time
  • Optimize or remove slow middleware

Debugging Middleware

To debug middleware execution order, add temporary logging:

php
final class DebugMiddleware implements MiddlewareInterface
{
    public function __construct(
        private string $name
    ) {}
    
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        error_log("Before {$this->name}");
        $response = $handler->handle($request);
        error_log("After {$this->name}");
        return $response;
    }
}

Register multiple instances:

php
return [
    'global_middlewares' => [
        new DebugMiddleware('First'),
        new DebugMiddleware('Second'),
        new DebugMiddleware('Third'),
    ],
];

This will show the execution order in your logs.

5.4 Middleware with Dependency Injection

ElliePHP's middleware system fully supports dependency injection through constructor injection. The container automatically resolves and injects dependencies when middleware is instantiated, making it easy to use services, repositories, and other dependencies in your middleware.

Constructor Injection in Middleware

Just like controllers and services, middleware can declare dependencies in their constructor, and the container will automatically inject them.

Basic Middleware with Dependency Injection:

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\SimpleCache\CacheInterface;

final readonly class CacheMiddleware implements MiddlewareInterface
{
    public function __construct(
        private CacheInterface $cache
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $cacheKey = $this->getCacheKey($request);
        
        // Try to get cached response
        $cached = $this->cache->get($cacheKey);
        
        if ($cached !== null) {
            return unserialize($cached);
        }
        
        // Get fresh response
        $response = $handler->handle($request);
        
        // Cache the response
        $this->cache->set($cacheKey, serialize($response), 300);
        
        return $response;
    }
    
    private function getCacheKey(ServerRequestInterface $request): string
    {
        return 'response:' . md5((string) $request->getUri());
    }
}

Middleware with Multiple Dependencies

Middleware can inject multiple services just like any other class:

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use ElliePHP\Framework\Application\Services\AuthService;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;

final readonly class AuthMiddleware implements MiddlewareInterface
{
    public function __construct(
        private AuthService $authService,
        private CacheInterface $cache,
        private LoggerInterface $logger
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $token = $request->getHeaderLine('Authorization');
        
        if (empty($token)) {
            $this->logger->warning('Missing authorization token', [
                'uri' => (string) $request->getUri(),
                'ip' => $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown'
            ]);
            
            return response()->json(['error' => 'Unauthorized'], 401);
        }
        
        // Check cache for token validation
        $cacheKey = "auth:token:{$token}";
        $userId = $this->cache->get($cacheKey);
        
        if ($userId === null) {
            // Validate token using auth service
            $userId = $this->authService->validateToken($token);
            
            if ($userId === null) {
                $this->logger->warning('Invalid authentication token', [
                    'token' => substr($token, 0, 10) . '...'
                ]);
                
                return response()->json(['error' => 'Invalid token'], 401);
            }
            
            // Cache valid token for 5 minutes
            $this->cache->set($cacheKey, $userId, 300);
        }
        
        // Add user ID to request
        $request = $request->withAttribute('user_id', $userId);
        
        $this->logger->info('User authenticated', ['user_id' => $userId]);
        
        return $handler->handle($request);
    }
}

Injecting Services into Middleware

Example: Rate Limiting with Cache Service

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\SimpleCache\CacheInterface;

final readonly class RateLimitMiddleware implements MiddlewareInterface
{
    public function __construct(
        private CacheInterface $cache,
        private int $maxRequests = 60,
        private int $windowSeconds = 60
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $ip = $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown';
        $key = "rate_limit:{$ip}";
        
        // Get current request count
        $requests = (int) $this->cache->get($key, 0);
        
        if ($requests >= $this->maxRequests) {
            return response()->json([
                'error' => 'Rate limit exceeded',
                'retry_after' => $this->windowSeconds
            ], 429)->withHeader('Retry-After', (string) $this->windowSeconds);
        }
        
        // Increment counter
        $this->cache->set($key, $requests + 1, $this->windowSeconds);
        
        // Add rate limit headers
        $response = $handler->handle($request);
        
        return $response
            ->withHeader('X-RateLimit-Limit', (string) $this->maxRequests)
            ->withHeader('X-RateLimit-Remaining', (string) ($this->maxRequests - $requests - 1));
    }
}

Example: Logging with Logger Service

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;

final readonly class RequestLoggerMiddleware implements MiddlewareInterface
{
    public function __construct(
        private LoggerInterface $logger
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $startTime = microtime(true);
        
        // Log incoming request
        $this->logger->info('Incoming request', [
            'method' => $request->getMethod(),
            'uri' => (string) $request->getUri(),
            'ip' => $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown'
        ]);
        
        // Process request
        $response = $handler->handle($request);
        
        // Log response
        $duration = microtime(true) - $startTime;
        
        $this->logger->info('Request completed', [
            'method' => $request->getMethod(),
            'uri' => (string) $request->getUri(),
            'status' => $response->getStatusCode(),
            'duration_ms' => round($duration * 1000, 2)
        ]);
        
        return $response;
    }
}

AuthMiddleware Example from DEPENDENCY_INJECTION.md

The framework documentation includes a complete AuthMiddleware example that demonstrates dependency injection:

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use ElliePHP\Framework\Application\Services\AuthService;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final readonly class AuthMiddleware implements MiddlewareInterface
{
    public function __construct(
        private AuthService $authService
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        if (!$this->authService->isAuthenticated($request)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        return $handler->handle($request);
    }
}

This example shows:

  • Constructor Injection: AuthService is automatically injected
  • Readonly Class: Middleware is immutable for thread safety
  • Service Usage: The injected service handles authentication logic
  • Clean Separation: Authentication logic is in the service, not the middleware

Injecting Repositories

Middleware can also inject repositories for data access:

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use ElliePHP\Framework\Application\Repositories\UserRepository;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final readonly class UserContextMiddleware implements MiddlewareInterface
{
    public function __construct(
        private UserRepository $userRepository
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $userId = $request->getAttribute('user_id');
        
        if ($userId !== null) {
            // Load user from repository
            $user = $this->userRepository->findById($userId);
            
            if ($user !== null) {
                // Add full user object to request
                $request = $request->withAttribute('user', $user);
            }
        }
        
        return $handler->handle($request);
    }
}

Injecting Configuration

You can inject configuration values through the container:

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final readonly class MaintenanceModeMiddleware implements MiddlewareInterface
{
    public function __construct(
        private bool $maintenanceMode,
        private string $maintenanceMessage
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        if ($this->maintenanceMode) {
            return response()->json([
                'error' => 'Service Unavailable',
                'message' => $this->maintenanceMessage
            ], 503);
        }
        
        return $handler->handle($request);
    }
}

Configure in configs/Container.php:

php
use function DI\create;

return [
    MaintenanceModeMiddleware::class => create(MaintenanceModeMiddleware::class)
        ->constructor(
            env('MAINTENANCE_MODE', false),
            env('MAINTENANCE_MESSAGE', 'System is under maintenance')
        ),
];

Complex Dependency Injection Example

Here's a comprehensive example with multiple dependencies and complex logic:

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use ElliePHP\Framework\Application\Services\AuthService;
use ElliePHP\Framework\Application\Services\PermissionService;
use ElliePHP\Framework\Application\Repositories\UserRepository;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;

final readonly class AuthorizationMiddleware implements MiddlewareInterface
{
    public function __construct(
        private AuthService $authService,
        private PermissionService $permissionService,
        private UserRepository $userRepository,
        private CacheInterface $cache,
        private LoggerInterface $logger
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Get user ID from request (set by AuthMiddleware)
        $userId = $request->getAttribute('user_id');
        
        if ($userId === null) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }
        
        // Get required permission from route
        $requiredPermission = $request->getAttribute('required_permission');
        
        if ($requiredPermission === null) {
            // No permission required, continue
            return $handler->handle($request);
        }
        
        // Check cache for user permissions
        $cacheKey = "user_permissions:{$userId}";
        $permissions = $this->cache->get($cacheKey);
        
        if ($permissions === null) {
            // Load user and permissions
            $user = $this->userRepository->findById($userId);
            
            if ($user === null) {
                $this->logger->error('User not found', ['user_id' => $userId]);
                return response()->json(['error' => 'User not found'], 404);
            }
            
            $permissions = $this->permissionService->getUserPermissions($user);
            
            // Cache for 5 minutes
            $this->cache->set($cacheKey, $permissions, 300);
        }
        
        // Check if user has required permission
        if (!in_array($requiredPermission, $permissions)) {
            $this->logger->warning('Permission denied', [
                'user_id' => $userId,
                'required_permission' => $requiredPermission,
                'user_permissions' => $permissions
            ]);
            
            return response()->json([
                'error' => 'Forbidden',
                'message' => 'You do not have permission to access this resource'
            ], 403);
        }
        
        $this->logger->info('Permission granted', [
            'user_id' => $userId,
            'permission' => $requiredPermission
        ]);
        
        return $handler->handle($request);
    }
}

Benefits of Dependency Injection in Middleware

Testability

  • Easy to mock dependencies in unit tests
  • Can test middleware logic in isolation
  • No need for complex test setup

Reusability

  • Services can be shared across middleware, controllers, and commands
  • Business logic stays in services, not middleware
  • Middleware remains focused on request/response processing

Flexibility

  • Easy to swap implementations (e.g., different cache drivers)
  • Can configure dependencies through container
  • Supports interface-based programming

Maintainability

  • Clear dependencies declared in constructor
  • No hidden dependencies or global state
  • Easy to understand what middleware needs

Best Practices

Use Readonly Classes

  • Make middleware readonly when possible
  • Promotes immutability and thread safety
  • Prevents accidental state mutations

Inject Interfaces, Not Implementations

  • Depend on CacheInterface, not specific cache classes
  • Depend on LoggerInterface, not Monolog
  • Makes testing and swapping implementations easier

Keep Middleware Thin

  • Delegate complex logic to services
  • Middleware should orchestrate, not implement
  • Business logic belongs in services, not middleware

Cache Expensive Operations

  • Use injected cache service for repeated lookups
  • Cache validation results, user data, permissions
  • Set appropriate TTL values

Log Important Events

  • Use injected logger for security events
  • Log authentication failures, permission denials
  • Include context for debugging

5.5 Built-in Middleware

ElliePHP includes two built-in middleware classes that provide essential functionality for web applications: CorsMiddleware for handling cross-origin requests and LoggingMiddleware for request/response analytics and tracing.

CorsMiddleware

The CorsMiddleware handles Cross-Origin Resource Sharing (CORS) by adding appropriate headers to responses, allowing your API to be accessed from different domains.

Location: app/Http/Middlewares/CorsMiddleware.php

Source Code:

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
 * CORS (Cross-Origin Resource Sharing) middleware
 *
 * Adds CORS headers to allow cross-origin requests.
 * Configure allowed origins, methods, and headers as needed.
 */
final class CorsMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $response = $handler->handle($request);

        return $response
            ->withHeader('Access-Control-Allow-Origin', '*')
            ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
            ->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    }
}

Functionality:

The CorsMiddleware adds three essential CORS headers to every response:

  1. Access-Control-Allow-Origin: Specifies which origins can access the resource

    • Default: * (allows all origins)
    • For production, consider restricting to specific domains
  2. Access-Control-Allow-Methods: Specifies which HTTP methods are allowed

    • Default: GET, POST, PUT, DELETE, OPTIONS
    • Covers standard RESTful API operations
  3. Access-Control-Allow-Headers: Specifies which headers can be used

    • Default: Content-Type, Authorization
    • Allows JSON requests and authentication headers

Configuration:

The default configuration allows all origins and common HTTP methods. To customize, modify the middleware:

Restrict to Specific Origins:

php
final class CorsMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $response = $handler->handle($request);

        return $response
            ->withHeader('Access-Control-Allow-Origin', 'https://yourdomain.com')
            ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
            ->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    }
}

Multiple Origins:

php
final class CorsMiddleware implements MiddlewareInterface
{
    private array $allowedOrigins = [
        'https://app.example.com',
        'https://admin.example.com',
        'http://localhost:3000', // Development
    ];

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $response = $handler->handle($request);
        
        $origin = $request->getHeaderLine('Origin');
        
        if (in_array($origin, $this->allowedOrigins)) {
            $response = $response->withHeader('Access-Control-Allow-Origin', $origin);
        }

        return $response
            ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
            ->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    }
}

Environment-Based Configuration:

php
final class CorsMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $response = $handler->handle($request);
        
        $allowedOrigin = env('CORS_ALLOWED_ORIGIN', '*');
        $allowedMethods = env('CORS_ALLOWED_METHODS', 'GET, POST, PUT, DELETE, OPTIONS');
        $allowedHeaders = env('CORS_ALLOWED_HEADERS', 'Content-Type, Authorization');

        return $response
            ->withHeader('Access-Control-Allow-Origin', $allowedOrigin)
            ->withHeader('Access-Control-Allow-Methods', $allowedMethods)
            ->withHeader('Access-Control-Allow-Headers', $allowedHeaders);
    }
}

Then configure in .env:

env
CORS_ALLOWED_ORIGIN=https://yourdomain.com
CORS_ALLOWED_METHODS=GET, POST, PUT, DELETE, OPTIONS
CORS_ALLOWED_HEADERS=Content-Type, Authorization, X-Custom-Header

Additional CORS Headers:

For more advanced CORS configuration, you can add additional headers:

php
return $response
    ->withHeader('Access-Control-Allow-Origin', $origin)
    ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
    ->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
    ->withHeader('Access-Control-Allow-Credentials', 'true')
    ->withHeader('Access-Control-Max-Age', '86400') // Cache preflight for 24 hours
    ->withHeader('Access-Control-Expose-Headers', 'X-Total-Count, X-Page-Count');

LoggingMiddleware

The LoggingMiddleware provides comprehensive request/response analytics and correlation ID tracking for distributed tracing.

Location: app/Http/Middlewares/LoggingMiddleware.php

Source Code:

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Random\RandomException;

/**
 * HTTP request/response logging middleware
 *
 * Logs detailed analytics for each HTTP request including:
 * - Request method, URI, and status code
 * - Response time and memory usage
 * - Request/response sizes
 * - Correlation ID for request tracing
 */
final class LoggingMiddleware implements MiddlewareInterface
{
    /**
     * @throws RandomException
     */
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $startTime = microtime(true);
        $startMemory = memory_get_usage(true);

        // Support for correlation/trace ID
        $correlationId = $request->getHeaderLine('X-Correlation-ID')
            ?: bin2hex(random_bytes(8));

        // Forward correlation ID downstream
        $request = $request->withAttribute('correlation_id', $correlationId);

        // Extract request size if possible
        $requestSize = $request->getBody()->getSize() ?? 0;

        $response = $handler->handle($request);

        $durationMs = round((microtime(true) - $startTime) * 1000, 2);
        $memoryDelta = memory_get_usage(true) - $startMemory;

        // Response size if PSR-7 implementation provides it
        $responseSize = $response->getBody()->getSize();

        // Extract route/controller info if your framework provides it
        $routeName = $request->getAttribute('route_name') ??
            $request->getAttribute('route') ??
            null;

        report()->info('HTTP Request Analytics', [
            'correlation_id' => $correlationId,
            'method' => $request->getMethod(),
            'uri' => (string) $request->getUri(),
            'status' => $response->getStatusCode(),
            'route' => $routeName,

            'duration_ms' => $durationMs,
            'memory_delta_bytes' => $memoryDelta,

            'request_size_bytes' => $requestSize,
            'response_size_bytes' => $responseSize,

            'client_ip' => $request->getServerParams()['REMOTE_ADDR'] ?? null,
            'user_agent' => $request->getHeaderLine('User-Agent'),
        ]);

        // Propagate correlation ID in response headers
        return $response->withHeader('X-Correlation-ID', $correlationId);
    }
}

Functionality:

The LoggingMiddleware provides comprehensive request/response analytics:

Performance Metrics:

  • Duration: Request processing time in milliseconds
  • Memory Delta: Memory used during request processing
  • Request Size: Size of incoming request body in bytes
  • Response Size: Size of outgoing response body in bytes

Request Information:

  • Method: HTTP method (GET, POST, PUT, DELETE, etc.)
  • URI: Full request URI
  • Route: Route name or pattern (if available)
  • Client IP: Remote client IP address
  • User Agent: Client user agent string

Response Information:

  • Status Code: HTTP response status code
  • Correlation ID: Unique identifier for request tracing

Analytics Data:

The middleware logs a structured analytics object:

json
{
  "correlation_id": "a1b2c3d4e5f6g7h8",
  "method": "POST",
  "uri": "http://localhost:8000/api/users",
  "status": 201,
  "route": "/api/users",
  "duration_ms": 45.23,
  "memory_delta_bytes": 524288,
  "request_size_bytes": 256,
  "response_size_bytes": 512,
  "client_ip": "127.0.0.1",
  "user_agent": "Mozilla/5.0..."
}

Correlation ID Tracking:

The correlation ID is a unique identifier that tracks a request through your system:

Incoming Request with Correlation ID:

bash
curl -H "X-Correlation-ID: my-custom-id" http://localhost:8000/api/users

The middleware will use the provided ID and include it in logs and the response.

Incoming Request without Correlation ID:

bash
curl http://localhost:8000/api/users

The middleware generates a random 16-character hex ID and includes it in logs and the response.

Response Header:

Every response includes the correlation ID:

X-Correlation-ID: a1b2c3d4e5f6g7h8

Benefits of Correlation IDs:

  • Distributed Tracing: Track requests across multiple services
  • Debugging: Find all logs related to a specific request
  • Error Investigation: Trace errors back to the original request
  • Performance Analysis: Identify slow requests across services

Using Correlation ID in Your Code:

Access the correlation ID from the request in controllers or other middleware:

php
public function store(): ResponseInterface
{
    $correlationId = request()->getAttribute('correlation_id');
    
    report()->info('Creating user', [
        'correlation_id' => $correlationId,
        'data' => request()->input()
    ]);
    
    // Your logic here
}

Log Output Example:

When a request is processed, you'll see a log entry like this:

[2024-11-17 10:30:45] app.INFO: HTTP Request Analytics {
  "correlation_id": "a1b2c3d4e5f6g7h8",
  "method": "GET",
  "uri": "http://localhost:8000/api/users/123",
  "status": 200,
  "route": "/api/users/{id}",
  "duration_ms": 12.45,
  "memory_delta_bytes": 262144,
  "request_size_bytes": 0,
  "response_size_bytes": 1024,
  "client_ip": "127.0.0.1",
  "user_agent": "curl/7.68.0"
}

Customization Examples

Custom CorsMiddleware with Dependency Injection:

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final readonly class CorsMiddleware implements MiddlewareInterface
{
    public function __construct(
        private array $allowedOrigins,
        private array $allowedMethods,
        private array $allowedHeaders
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $response = $handler->handle($request);
        
        $origin = $request->getHeaderLine('Origin');
        
        // Check if origin is allowed
        if (in_array($origin, $this->allowedOrigins) || in_array('*', $this->allowedOrigins)) {
            $allowedOrigin = in_array('*', $this->allowedOrigins) ? '*' : $origin;
            
            $response = $response
                ->withHeader('Access-Control-Allow-Origin', $allowedOrigin)
                ->withHeader('Access-Control-Allow-Methods', implode(', ', $this->allowedMethods))
                ->withHeader('Access-Control-Allow-Headers', implode(', ', $this->allowedHeaders));
        }

        return $response;
    }
}

Configure in configs/Container.php:

php
use function DI\create;

return [
    CorsMiddleware::class => create(CorsMiddleware::class)
        ->constructor(
            ['https://app.example.com', 'https://admin.example.com'],
            ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
            ['Content-Type', 'Authorization', 'X-Custom-Header']
        ),
];

Enhanced LoggingMiddleware with Additional Context:

php
<?php

namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;

final readonly class LoggingMiddleware implements MiddlewareInterface
{
    public function __construct(
        private LoggerInterface $logger
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $startTime = microtime(true);
        $startMemory = memory_get_usage(true);

        $correlationId = $request->getHeaderLine('X-Correlation-ID')
            ?: bin2hex(random_bytes(8));

        $request = $request->withAttribute('correlation_id', $correlationId);

        // Log request start
        $this->logger->info('Request started', [
            'correlation_id' => $correlationId,
            'method' => $request->getMethod(),
            'uri' => (string) $request->getUri(),
        ]);

        $response = $handler->handle($request);

        $durationMs = round((microtime(true) - $startTime) * 1000, 2);
        $memoryDelta = memory_get_usage(true) - $startMemory;

        // Log request completion with full analytics
        $this->logger->info('Request completed', [
            'correlation_id' => $correlationId,
            'method' => $request->getMethod(),
            'uri' => (string) $request->getUri(),
            'status' => $response->getStatusCode(),
            'duration_ms' => $durationMs,
            'memory_delta_bytes' => $memoryDelta,
            'client_ip' => $request->getServerParams()['REMOTE_ADDR'] ?? null,
        ]);

        return $response->withHeader('X-Correlation-ID', $correlationId);
    }
}

Using Built-in Middleware

Both middleware are registered by default in configs/Middleware.php:

php
<?php

return [
    'global_middlewares' => [
        \ElliePHP\Framework\Application\Http\Middlewares\LoggingMiddleware::class,
        \ElliePHP\Framework\Application\Http\Middlewares\CorsMiddleware::class,
    ],
];

To disable a middleware, simply remove it from the array:

php
<?php

return [
    'global_middlewares' => [
        // LoggingMiddleware removed
        \ElliePHP\Framework\Application\Http\Middlewares\CorsMiddleware::class,
    ],
];

To replace with custom version, use your own class:

php
<?php

return [
    'global_middlewares' => [
        \ElliePHP\Framework\Application\Http\Middlewares\LoggingMiddleware::class,
        \App\Middlewares\CustomCorsMiddleware::class, // Custom implementation
    ],
];

Best Practices

CorsMiddleware:

  • Restrict origins in production (avoid *)
  • Only allow necessary HTTP methods
  • Only allow required headers
  • Consider using environment variables for configuration
  • Test with actual frontend applications

LoggingMiddleware:

  • Keep it first in the middleware stack to capture all requests
  • Use correlation IDs for distributed tracing
  • Monitor log volume in high-traffic applications
  • Consider log rotation and retention policies
  • Use structured logging for easy parsing and analysis

6. HTTP Request & Response

6.1 Request Class

6.1 Request Class

The Request class provides a clean, type-safe interface for accessing HTTP request data. It wraps PSR-7 ServerRequestInterface and provides convenient methods for retrieving input, headers, cookies, and more.

Creating Request Instances

From Global Variables

The most common way to create a request is from PHP's global variables:

php
use ElliePHP\Components\Support\Http\Request;

$request = Request::fromGlobals();

This method automatically captures data from $_GET, $_POST, $_SERVER, $_COOKIE, and $_FILES.

Manual Creation

For testing or custom scenarios, create requests manually:

php
$request = Request::create(
    method: 'POST',
    uri: '/api/users',
    headers: ['Content-Type' => 'application/json'],
    body: json_encode(['name' => 'John Doe']),
    version: '1.1'
);

Accessing Request Data

Input Method with Type Casting

The input() method retrieves data from both query parameters and POST body:

php
// Get input value
$name = $request->input('name');

// With default value
$page = $request->input('page', 1);

// Get all input
$allData = $request->input();

// Get only specific keys
$data = $request->input(['name', 'email']);

Type Helper Methods

ElliePHP provides type-safe methods for casting input values:

php
// Get as string
$name = $request->string('name', 'Guest');

// Get as integer
$age = $request->int('age', 0);
$userId = $request->integer('user_id', 0); // Alias

// Get as boolean
$active = $request->bool('active', false);
$enabled = $request->boolean('enabled', false); // Alias

// Get as float
$price = $request->float('price', 0.0);

// Get as array
$tags = $request->array('tags', []);

// Get as date
$birthdate = $request->date('birthdate', 'Y-m-d');

Query and POST Parameters

Access query and POST data separately:

php
// Query parameters (?page=1&limit=10)
$page = $request->query('page', 1);
$allQuery = $request->allQuery();

// POST parameters
$username = $request->post('username');
$allPost = $request->allPost();

// All input (query + POST)
$all = $request->all();

Checking Input Existence

php
// Check if key exists
if ($request->has('email')) {
    // Key exists
}

// Check multiple keys
if ($request->has(['name', 'email'])) {
    // All keys exist
}

// Check if any key exists
if ($request->hasAny(['email', 'phone'])) {
    // At least one exists
}

// Check if filled (exists and not empty)
if ($request->filled('name')) {
    // Name exists and is not empty
}

// Check if missing
if ($request->missing('optional_field')) {
    // Field doesn't exist
}

Filtering Input

php
// Get only specific keys
$credentials = $request->only(['email', 'password']);

// Get all except specific keys
$userData = $request->except(['password', 'password_confirmation']);

// Get when filled, otherwise default
$bio = $request->whenFilled('bio', 'No bio provided');

Request Information Methods

HTTP Method

php
// Get request method
$method = $request->method(); // 'GET', 'POST', etc.

// Check specific method
if ($request->isMethod('POST')) {
    // Handle POST request
}

// Convenience methods
if ($request->isGet()) { }
if ($request->isPost()) { }
if ($request->isPut()) { }
if ($request->isPatch()) { }
if ($request->isDelete()) { }

URL and Path

php
// Get full URI
$uri = $request->uri();
$url = $request->url(); // Alias

// Get path only
$path = $request->path(); // '/api/users'

// Get full URL with query string
$fullUrl = $request->fullUrl();

// Get URL without query parameters
$baseUrl = $request->urlWithoutQuery();

// Get scheme, host, port
$scheme = $request->scheme(); // 'http' or 'https'
$host = $request->host(); // 'example.com'
$port = $request->port(); // 80, 443, or custom

Content Type Detection

php
// Check if JSON request
if ($request->isJson()) {
    $data = $request->json();
}

// Check if expects JSON response
if ($request->expectsJson()) {
    return response()->json($data);
}

// Check if wants JSON (Accept header)
if ($request->wantsJson()) {
    // Client prefers JSON
}

// Check if AJAX request
if ($request->isAjax()) {
    // Handle AJAX
}

Header Access Methods

Getting Headers

php
// Get specific header
$contentType = $request->header('Content-Type');

// With default value
$userAgent = $request->header('User-Agent', 'Unknown');

// Get all headers
$headers = $request->headers();

// Check if header exists
if ($request->hasHeader('Authorization')) {
    // Header exists
}

Bearer Token

Extract bearer tokens from the Authorization header:

php
// Get bearer token
$token = $request->bearerToken();

if ($token) {
    // Validate token
    $user = authenticateToken($token);
}

This extracts the token from headers like:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Common Headers

php
// User agent
$userAgent = $request->userAgent();

// Referer
$referer = $request->referer();
$referrer = $request->referrer(); // Alias

JSON Handling

Parse JSON request bodies:

php
// Get JSON as array
$data = $request->json(assoc: true);

// Get JSON as object
$data = $request->json(assoc: false);

// Example usage
if ($request->isJson()) {
    $user = $request->json();
    $name = $user['name'];
    $email = $user['email'];
}

File Uploads

php
// Get all uploaded files
$files = $request->files();
$allFiles = $request->allFiles(); // Alias

// Get specific file
$avatar = $request->file('avatar');

// Check if file exists
if ($request->hasFile('avatar')) {
    $file = $request->file('avatar');
    // Process file upload
}

Cookies and Server Data

Cookies

php
// Get all cookies
$cookies = $request->cookies();

// Get specific cookie
$sessionId = $request->cookie('session_id');

// With default value
$theme = $request->cookie('theme', 'light');

Server Parameters

php
// Get all server parameters
$server = $request->server();

// Get client IP
$ip = $request->ip();

// Get all client IPs (including proxies)
$ips = $request->ips();

Security Methods

php
// Check if HTTPS
if ($request->isSecure()) {
    // Secure connection
}

Request Attributes

Store and retrieve custom attributes on the request:

php
// Set attribute
$request = $request->withAttribute('user_id', 123);
$request = $request->set('user_id', 123); // Alias

// Get attribute
$userId = $request->attribute('user_id');
$userId = $request->get('user_id'); // Alias

// Get all attributes
$attributes = $request->attributes();

// Merge multiple attributes
$request = $request->merge([
    'user_id' => 123,
    'role' => 'admin'
]);

PSR-7 Access

Access the underlying PSR-7 request:

php
// Get PSR-7 ServerRequestInterface
$psrRequest = $request->psr();
$psrRequest = $request->raw(); // Alias

Complete Example

php
use ElliePHP\Components\Support\Http\Request;

$request = Request::fromGlobals();

// Validate and extract input
if ($request->isPost() && $request->has(['email', 'password'])) {
    $email = $request->string('email');
    $password = $request->string('password');
    $remember = $request->bool('remember', false);
    
    // Authenticate user
    if (authenticate($email, $password)) {
        // Success
    }
}

// API request handling
if ($request->isJson() && $request->bearerToken()) {
    $token = $request->bearerToken();
    $data = $request->json();
    
    // Process API request
}

6.2 Request Helper Function

The request() helper function provides quick access to the current HTTP request instance throughout your application.

Basic Usage

Getting the Request Instance

php
// Get current request
$request = request();

// Access request data
$name = request()->input('name');
$email = request()->string('email');

In Controllers

php
use ElliePHP\Components\Support\Http\Request;
use Psr\Http\Message\ResponseInterface;

final readonly class UserController
{
    public function store(): ResponseInterface
    {
        // Use helper function
        $name = request()->string('name');
        $email = request()->string('email');
        
        // Create user
        $user = User::create([
            'name' => $name,
            'email' => $email
        ]);
        
        return response()->json($user, 201);
    }
}

In Middleware

php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final readonly class ApiKeyMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Access via helper
        $apiKey = request()->header('X-API-Key');
        
        if (!$this->isValidApiKey($apiKey)) {
            return response()->unauthorized('Invalid API key');
        }
        
        return $handler->handle($request);
    }
}

Usage Examples

Form Validation

php
// Validate form input
if (request()->filled(['name', 'email', 'password'])) {
    $data = request()->only(['name', 'email', 'password']);
    // Process form
} else {
    return response()->badRequest('Missing required fields');
}

API Authentication

php
// Check bearer token
$token = request()->bearerToken();

if (!$token) {
    return response()->unauthorized('Token required');
}

// Validate token
$user = validateToken($token);

Content Negotiation

php
// Return appropriate response format
if (request()->wantsJson()) {
    return response()->json($data);
} else {
    return response()->html($htmlView);
}

Query Parameters

php
// Pagination
$page = request()->int('page', 1);
$limit = request()->int('limit', 10);

$users = User::paginate($page, $limit);

Singleton Behavior

The request() helper returns a singleton instance created from globals on first call:

php
// First call creates instance from globals
$request1 = request();

// Subsequent calls return same instance
$request2 = request();

// $request1 === $request2 (same instance)

This ensures consistent request data throughout the request lifecycle.

6.3 Response Class Basics

The Response class provides a fluent interface for creating HTTP responses. It wraps PSR-7 ResponseInterface and offers convenient methods for common response types.

Creating Responses

Basic Response Creation

php
use ElliePHP\Components\Support\Http\Response;
use Nyholm\Psr7\Factory\Psr17Factory;

// Create response with PSR-7 factory
$factory = new Psr17Factory();
$psrResponse = $factory->createResponse(200);
$response = new Response($psrResponse);

Using the make() Method

The make() method creates responses with automatic content type detection:

php
// String content (text/plain)
$response = $response->make('Hello World', 200);

// Array/object content (application/json)
$response = $response->make(['message' => 'Success'], 200);

// With custom headers
$response = $response->make(
    content: 'Hello',
    status: 200,
    headers: ['X-Custom-Header' => 'value']
);

Auto-Detection

The make() method automatically detects content type:

php
// Automatically returns JSON
$response->make(['user' => $user]);

// Automatically returns text
$response->make('Plain text content');

PSR-7 Response Wrapping

ElliePHP's Response class wraps PSR-7 responses, providing both convenience methods and full PSR-7 compatibility:

php
// Access underlying PSR-7 response
$psrResponse = $response->psr();
$psrResponse = $response->raw(); // Alias

// PSR-7 methods still available
$statusCode = $psrResponse->getStatusCode();
$headers = $psrResponse->getHeaders();
$body = $psrResponse->getBody();

Why PSR-7?

PSR-7 responses are:

  • Immutable: Each modification returns a new instance
  • Interoperable: Work with any PSR-7 compatible library
  • Testable: Easy to mock and assert in tests
  • Standard: Follow PHP-FIG standards

Response Modification

Setting Status Code

php
// Set status code
$response = $response->withStatus(404, 'Not Found');
$response = $response->setStatusCode(404); // Alias

// Get status code
$code = $response->status();
$code = $response->getStatusCode(); // Alias

Setting Body Content

php
// Set body content
$response = $response->withBody('New content');
$response = $response->setContent('New content'); // Alias

// Get body content
$content = $response->body();
$content = $response->content(); // Alias

Immutability

PSR-7 responses are immutable - each modification returns a new instance:

php
$response1 = response()->make('Hello');
$response2 = $response1->withStatus(404);

// $response1 still has status 200
// $response2 has status 404
// They are different instances

Method Chaining

Take advantage of immutability with method chaining:

php
return response()
    ->json(['message' => 'Success'])
    ->withHeader('X-Request-ID', $requestId)
    ->withStatus(201);

Complete Example

php
use ElliePHP\Components\Support\Http\Response;
use Nyholm\Psr7\Factory\Psr17Factory;

// Create response instance
$factory = new Psr17Factory();
$psrResponse = $factory->createResponse(200);
$response = new Response($psrResponse);

// Create JSON response
$jsonResponse = $response->json([
    'status' => 'success',
    'data' => $users
]);

// Create HTML response
$htmlResponse = $response->html('<h1>Welcome</h1>');

// Create with custom status and headers
$customResponse = $response->make(
    content: ['error' => 'Not found'],
    status: 404,
    headers: [
        'X-Error-Code' => 'USER_NOT_FOUND',
        'X-Request-ID' => $requestId
    ]
);

6.4 Response Content Types

ElliePHP provides dedicated methods for creating responses with specific content types.

JSON Responses

Basic JSON

php
// Simple JSON response
return response()->json(['message' => 'Success']);

// With status code
return response()->json(['user' => $user], 201);

// With custom headers
return response()->json(
    data: ['users' => $users],
    status: 200,
    headers: ['X-Total-Count' => count($users)]
);

// With custom JSON flags
return response()->json(
    data: $data,
    status: 200,
    headers: [],
    flags: JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
);

JSON API Responses

php
// Success response
return response()->json([
    'status' => 'success',
    'data' => $user,
    'message' => 'User created successfully'
], 201);

// Error response
return response()->json([
    'status' => 'error',
    'message' => 'Validation failed',
    'errors' => $validationErrors
], 422);

// Paginated response
return response()->json([
    'data' => $users,
    'meta' => [
        'current_page' => $page,
        'per_page' => $limit,
        'total' => $total
    ]
]);

JSONP Responses

For cross-domain requests with callback functions:

php
// JSONP response
return response()->jsonp(
    callback: 'handleResponse',
    data: ['users' => $users]
);

// With status and headers
return response()->jsonp(
    callback: 'myCallback',
    data: $data,
    status: 200,
    headers: ['X-Custom' => 'value']
);

This generates:

javascript
/**/ typeof handleResponse === 'function' && handleResponse({"users":[...]});

HTML Responses

Basic HTML

php
// Simple HTML
return response()->html('<h1>Welcome</h1>');

// With status code
return response()->html($htmlContent, 200);

// With headers
return response()->html(
    html: $template,
    status: 200,
    headers: ['X-Frame-Options' => 'DENY']
);

Complete HTML Pages

php
$html = <<<HTML
<!DOCTYPE html>
<html>
<head>
    <title>Welcome</title>
</head>
<body>
    <h1>Welcome to ElliePHP</h1>
    <p>A fast, modular PHP microframework.</p>
</body>
</html>
HTML;

return response()->html($html);

Plain Text Responses

php
// Simple text
return response()->text('Hello, World!');

// With status
return response()->text('Not Found', 404);

// With headers
return response()->text(
    text: 'Server log output',
    status: 200,
    headers: ['Content-Disposition' => 'attachment; filename="log.txt"']
);

XML Responses

php
// XML response
$xml = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<users>
    <user>
        <id>1</id>
        <name>John Doe</name>
    </user>
</users>
XML;

return response()->xml($xml);

// With status and headers
return response()->xml(
    xml: $xmlContent,
    status: 200,
    headers: ['X-API-Version' => '1.0']
);

Content Type Headers

Each method automatically sets the appropriate Content-Type header:

  • json(): application/json
  • jsonp(): text/javascript
  • html(): text/html; charset=utf-8
  • text(): text/plain; charset=utf-8
  • xml(): application/xml; charset=utf-8

You can override these with custom headers:

php
return response()->json(
    data: $data,
    headers: ['Content-Type' => 'application/vnd.api+json']
);

6.5 Response Redirects

ElliePHP provides several methods for creating HTTP redirect responses with different status codes.

Basic Redirect

Standard Redirect (302)

php
// Temporary redirect (302)
return response()->redirect('/dashboard');

// With custom status
return response()->redirect('/login', 301);

// With headers
return response()->redirect(
    url: '/profile',
    status: 302,
    headers: ['X-Redirect-Reason' => 'authentication']
);

Redirect Methods

Permanent Redirect (301)

Use for permanent URL changes (SEO-friendly):

php
// 301 Moved Permanently
return response()->redirectPermanent('/new-url');

// With headers
return response()->redirectPermanent(
    url: '/new-location',
    headers: ['X-Moved-From' => '/old-location']
);

Temporary Redirect (302)

Use for temporary redirects:

php
// 302 Found (temporary)
return response()->redirectTemporary('/maintenance');

// With headers
return response()->redirectTemporary(
    url: '/temp-page',
    headers: ['Retry-After' => '3600']
);

See Other Redirect (303)

Use after POST requests to redirect to a GET endpoint:

php
// 303 See Other
return response()->redirectSeeOther('/users/123');

// Common pattern after form submission
public function store(): ResponseInterface
{
    $user = User::create(request()->all());
    
    // Redirect to GET endpoint after POST
    return response()->redirectSeeOther('/users/' . $user->id);
}

Back Redirect

Redirect to the previous URL (from Referer header):

php
// Redirect back
return response()->back();

// With fallback URL
return response()->back('/dashboard');

// With custom status
return response()->back(
    fallback: '/home',
    status: 302
);

Use Cases

php
// After form validation failure
if ($validationFails) {
    return response()->back('/form');
}

// After successful operation
if ($success) {
    return response()->back('/dashboard');
}

Redirect Status Codes

Different redirect status codes have different meanings:

  • 301 Moved Permanently: Resource permanently moved, update bookmarks
  • 302 Found: Temporary redirect, original URL still valid
  • 303 See Other: Redirect to GET after POST/PUT/DELETE
  • 307 Temporary Redirect: Like 302 but preserves request method
  • 308 Permanent Redirect: Like 301 but preserves request method
php
// 301: Permanent, SEO-friendly
response()->redirect('/new-url', 301);
response()->redirectPermanent('/new-url');

// 302: Temporary, default
response()->redirect('/temp-url', 302);
response()->redirectTemporary('/temp-url');

// 303: After form submission
response()->redirect('/success', 303);
response()->redirectSeeOther('/success');

Complete Redirect Examples

After Login

php
public function login(): ResponseInterface
{
    $credentials = request()->only(['email', 'password']);
    
    if (auth()->attempt($credentials)) {
        return response()->redirect('/dashboard');
    }
    
    return response()->back('/login');
}

After Resource Creation

php
public function store(): ResponseInterface
{
    $user = User::create(request()->all());
    
    // Redirect to view the created resource
    return response()->redirectSeeOther('/users/' . $user->id);
}

URL Migration

php
// Old URL permanently moved to new URL
Router::get('/old-blog/{slug}', function($slug) {
    return response()->redirectPermanent('/articles/' . $slug);
});

6.6 Response Status Code Helpers

ElliePHP provides convenient helper methods for common HTTP status codes, making your code more readable and self-documenting.

2xx Success Responses

200 OK

php
// Standard success response
return response()->ok(['message' => 'Success']);

// With data
return response()->ok($user);

// With headers
return response()->ok(
    content: $data,
    headers: ['X-Request-ID' => $requestId]
);

201 Created

Use after successfully creating a resource:

php
// Resource created
return response()->created(['user' => $user]);

// With Location header
return response()->created(
    content: $user,
    headers: ['Location' => '/users/' . $user->id]
);

202 Accepted

Use when request is accepted but processing is not complete:

php
// Async processing accepted
return response()->accepted([
    'message' => 'Job queued',
    'job_id' => $jobId
]);

204 No Content

Use when operation succeeds but no content to return:

php
// Successful deletion
return response()->noContent();

// Alias
return response()->empty();

// With custom status
return response()->noContent(204);

4xx Client Error Responses

400 Bad Request

php
// Invalid request
return response()->badRequest('Invalid input');

// With validation errors
return response()->badRequest([
    'message' => 'Validation failed',
    'errors' => $errors
]);

401 Unauthorized

php
// Authentication required
return response()->unauthorized('Authentication required');

// With custom message
return response()->unauthorized([
    'message' => 'Invalid credentials'
]);

// With WWW-Authenticate header
return response()->unauthorized(
    content: 'Token expired',
    headers: ['WWW-Authenticate' => 'Bearer realm="API"']
);

403 Forbidden

php
// Access denied
return response()->forbidden('Access denied');

// With reason
return response()->forbidden([
    'message' => 'Insufficient permissions',
    'required_role' => 'admin'
]);

404 Not Found

php
// Resource not found
return response()->notFound('User not found');

// With details
return response()->notFound([
    'message' => 'Resource not found',
    'resource' => 'user',
    'id' => $id
]);

405 Method Not Allowed

php
// Method not allowed
return response()->methodNotAllowed(
    allowed: ['GET', 'POST'],
    content: 'Method not allowed'
);

// Automatically sets Allow header
return response()->methodNotAllowed(['GET', 'HEAD']);

409 Conflict

php
// Resource conflict
return response()->conflict('Email already exists');

// With details
return response()->conflict([
    'message' => 'Resource conflict',
    'field' => 'email',
    'value' => $email
]);

422 Unprocessable Entity

php
// Validation failed
return response()->unprocessable([
    'message' => 'Validation failed',
    'errors' => [
        'email' => ['Email is required'],
        'password' => ['Password must be at least 8 characters']
    ]
]);

429 Too Many Requests

php
// Rate limit exceeded
return response()->tooManyRequests(
    retryAfter: 60,
    content: 'Rate limit exceeded'
);

// Automatically sets Retry-After header
return response()->tooManyRequests(
    retryAfter: 3600,
    content: ['message' => 'Too many requests', 'limit' => 100]
);

5xx Server Error Responses

500 Internal Server Error

php
// Server error
return response()->serverError('An error occurred');

// With error details (in development)
return response()->serverError([
    'message' => 'Internal server error',
    'error' => $exception->getMessage()
]);

503 Service Unavailable

php
// Service unavailable
return response()->serviceUnavailable(
    retryAfter: 300,
    content: 'Service temporarily unavailable'
);

// Maintenance mode
return response()->serviceUnavailable(
    retryAfter: 3600,
    content: ['message' => 'Under maintenance', 'eta' => '1 hour']
);

Usage Examples

RESTful API Controller

php
final readonly class UserController
{
    public function __construct(
        private UserService $userService
    ) {}
    
    public function index(): ResponseInterface
    {
        $users = $this->userService->all();
        return response()->ok($users);
    }
    
    public function show(int $id): ResponseInterface
    {
        $user = $this->userService->find($id);
        
        if (!$user) {
            return response()->notFound('User not found');
        }
        
        return response()->ok($user);
    }
    
    public function store(): ResponseInterface
    {
        $data = request()->all();
        
        if (!$this->validate($data)) {
            return response()->unprocessable([
                'errors' => $this->errors
            ]);
        }
        
        $user = $this->userService->create($data);
        
        return response()->created($user);
    }
    
    public function update(int $id): ResponseInterface
    {
        $user = $this->userService->find($id);
        
        if (!$user) {
            return response()->notFound('User not found');
        }
        
        $updated = $this->userService->update($id, request()->all());
        
        return response()->ok($updated);
    }
    
    public function destroy(int $id): ResponseInterface
    {
        $user = $this->userService->find($id);
        
        if (!$user) {
            return response()->notFound('User not found');
        }
        
        $this->userService->delete($id);
        
        return response()->noContent();
    }
}

Authentication Middleware

php
public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
): ResponseInterface {
    $token = request()->bearerToken();
    
    if (!$token) {
        return response()->unauthorized('Token required');
    }
    
    $user = $this->validateToken($token);
    
    if (!$user) {
        return response()->unauthorized('Invalid token');
    }
    
    if (!$user->hasPermission($this->requiredPermission)) {
        return response()->forbidden('Insufficient permissions');
    }
    
    return $handler->handle($request);
}

6.7 File Downloads

ElliePHP provides multiple methods for serving file downloads with proper headers and streaming support.

Download from String

Basic Download

php
// Download content as file
$content = 'File content here';
return response()->download(
    content: $content,
    filename: 'document.txt'
);

// With custom headers
return response()->download(
    content: $pdfContent,
    filename: 'invoice.pdf',
    headers: ['X-Document-ID' => $documentId]
);

This automatically sets:

  • Content-Type: application/octet-stream
  • Content-Disposition: attachment; filename="document.txt"
  • Content-Length: [size]

Download from File

File Download

php
// Download existing file
return response()->file(
    path: '/path/to/file.pdf',
    filename: 'document.pdf'
);

// Use original filename
return response()->file('/path/to/report.xlsx');

// With custom headers
return response()->file(
    path: $filePath,
    filename: 'custom-name.pdf',
    headers: ['X-File-Version' => '1.0']
);

Error Handling

php
$filePath = storage_path('documents/' . $id . '.pdf');

if (!file_exists($filePath)) {
    return response()->notFound('File not found');
}

return response()->file($filePath);

Stream Download

Streaming Large Files

For large files, use streaming to avoid memory issues:

php
// Stream file download
return response()->streamDownload(
    path: '/path/to/large-file.zip',
    filename: 'archive.zip'
);

// With headers
return response()->streamDownload(
    path: $filePath,
    filename: 'backup.sql',
    headers: ['X-Backup-Date' => date('Y-m-d')]
);

Delete After Download

Automatically delete temporary files after download:

php
// Delete file after sending
return response()->streamDownload(
    path: $tempFilePath,
    filename: 'export.csv',
    deleteAfter: true
);

This is useful for temporary exports:

php
public function export(): ResponseInterface
{
    // Generate temporary file
    $tempFile = tempnam(sys_get_temp_dir(), 'export_');
    file_put_contents($tempFile, $this->generateCsv());
    
    // Stream and delete
    return response()->streamDownload(
        path: $tempFile,
        filename: 'users_export.csv',
        deleteAfter: true
    );
}

Content Types

Stream download automatically detects MIME type:

php
// PDF: application/pdf
response()->streamDownload('/path/to/file.pdf', 'document.pdf');

// ZIP: application/zip
response()->streamDownload('/path/to/archive.zip', 'files.zip');

// Image: image/jpeg, image/png, etc.
response()->streamDownload('/path/to/photo.jpg', 'photo.jpg');

// Fallback: application/octet-stream
response()->streamDownload('/path/to/unknown', 'file.bin');

Complete Examples

Document Download

php
public function downloadInvoice(int $id): ResponseInterface
{
    $invoice = Invoice::find($id);
    
    if (!$invoice) {
        return response()->notFound('Invoice not found');
    }
    
    // Check permissions
    if (!$this->canDownload($invoice)) {
        return response()->forbidden('Access denied');
    }
    
    $filePath = storage_path('invoices/' . $invoice->filename);
    
    return response()->file(
        path: $filePath,
        filename: 'invoice_' . $invoice->number . '.pdf'
    );
}

CSV Export

php
public function exportUsers(): ResponseInterface
{
    $users = User::all();
    
    // Generate CSV content
    $csv = "Name,Email,Created\n";
    foreach ($users as $user) {
        $csv .= sprintf(
            "%s,%s,%s\n",
            $user->name,
            $user->email,
            $user->created_at
        );
    }
    
    return response()->download(
        content: $csv,
        filename: 'users_' . date('Y-m-d') . '.csv',
        headers: ['Content-Type' => 'text/csv']
    );
}

Temporary File Export

php
public function exportReport(): ResponseInterface
{
    // Generate report in temporary file
    $tempFile = tempnam(sys_get_temp_dir(), 'report_');
    
    $handle = fopen($tempFile, 'w');
    fputcsv($handle, ['Column1', 'Column2', 'Column3']);
    
    foreach ($this->getData() as $row) {
        fputcsv($handle, $row);
    }
    
    fclose($handle);
    
    // Stream and auto-delete
    return response()->streamDownload(
        path: $tempFile,
        filename: 'report_' . date('Y-m-d_His') . '.csv',
        deleteAfter: true
    );
}

6.8 Headers and Cookies

ElliePHP provides comprehensive methods for managing HTTP headers and cookies in responses.

Header Management

Adding Single Header

php
// Add header
$response = response()->json($data)
    ->withHeader('X-API-Version', '1.0');

// Alias
$response = response()->json($data)
    ->header('X-Custom-Header', 'value');

Adding Multiple Headers

php
// Add multiple headers
$response = response()->json($data)
    ->withHeaders([
        'X-API-Version' => '1.0',
        'X-Request-ID' => $requestId,
        'X-RateLimit-Limit' => '100'
    ]);

Content Type Helper

php
// Set content type
$response = response()->make($content)
    ->contentType('application/vnd.api+json');

// Common content types
$response->contentType('application/json');
$response->contentType('text/html; charset=utf-8');
$response->contentType('application/xml');

Cache Control

Cache Control Header

php
// Set cache control
$response = response()->json($data)
    ->cacheControl('public, max-age=3600');

// No cache
$response = response()->json($data)
    ->cacheControl('no-cache, no-store, must-revalidate');

No Cache Helper

php
// Disable caching completely
$response = response()->json($data)
    ->noCache();

This sets:

  • Cache-Control: no-cache, no-store, must-revalidate
  • Pragma: no-cache
  • Expires: 0

ETag and Last-Modified

ETag Header

php
// Set ETag for cache validation
$etag = md5(json_encode($data));

$response = response()->json($data)
    ->etag($etag);

// Check if client has cached version
$clientEtag = request()->header('If-None-Match');
if ($clientEtag === $etag) {
    return response()->noContent(304); // Not Modified
}

Last-Modified Header

php
// Set last modified timestamp
$lastModified = $resource->updated_at->getTimestamp();

$response = response()->json($data)
    ->lastModified($lastModified);

// With DateTimeInterface
$response = response()->json($data)
    ->lastModified($resource->updated_at);

// Check if modified since
$ifModifiedSince = request()->header('If-Modified-Since');
if ($ifModifiedSince && strtotime($ifModifiedSince) >= $lastModified) {
    return response()->noContent(304);
}

Setting Cookies

php
// Set cookie (expires in minutes)
$response = response()->json($data)
    ->cookie(
        name: 'session_id',
        value: $sessionId,
        minutes: 60
    );

// With all options
$response = response()->json($data)
    ->cookie(
        name: 'user_pref',
        value: 'dark_mode',
        minutes: 43200, // 30 days
        path: '/',
        domain: '.example.com',
        secure: true,
        httpOnly: true,
        sameSite: 'Strict'
    );

Cookie with Expiration Time

php
// Using withCookie (expires in seconds)
$expiresAt = time() + 3600; // 1 hour

$response = response()->json($data)
    ->withCookie(
        name: 'token',
        value: $token,
        expires: $expiresAt,
        path: '/',
        domain: '',
        secure: true,
        httpOnly: true,
        sameSite: 'Lax'
    );

Deleting Cookies

php
// Delete cookie
$response = response()->json($data)
    ->withoutCookie('session_id');

// With path and domain
$response = response()->json($data)
    ->withoutCookie(
        name: 'user_token',
        path: '/api',
        domain: '.example.com'
    );

Complete Examples

API Response with Headers

php
public function index(): ResponseInterface
{
    $users = User::paginate($page, $limit);
    
    return response()->json([
        'data' => $users,
        'meta' => [
            'page' => $page,
            'total' => $total
        ]
    ])->withHeaders([
        'X-Total-Count' => $total,
        'X-Page' => $page,
        'X-Per-Page' => $limit,
        'X-Request-ID' => $requestId
    ]);
}

Cached Response

php
public function show(int $id): ResponseInterface
{
    $user = User::find($id);
    
    if (!$user) {
        return response()->notFound('User not found');
    }
    
    $etag = md5(json_encode($user) . $user->updated_at);
    $lastModified = $user->updated_at->getTimestamp();
    
    // Check client cache
    if (request()->header('If-None-Match') === $etag) {
        return response()->noContent(304);
    }
    
    return response()->json($user)
        ->etag($etag)
        ->lastModified($lastModified)
        ->cacheControl('public, max-age=300');
}

Authentication Response with Cookie

php
public function login(): ResponseInterface
{
    $credentials = request()->only(['email', 'password']);
    
    if (!$token = auth()->attempt($credentials)) {
        return response()->unauthorized('Invalid credentials');
    }
    
    return response()->json([
        'token' => $token,
        'user' => auth()->user()
    ])->cookie(
        name: 'auth_token',
        value: $token,
        minutes: 60 * 24 * 7, // 7 days
        httpOnly: true,
        secure: true,
        sameSite: 'Strict'
    );
}

Logout Response

php
public function logout(): ResponseInterface
{
    auth()->logout();
    
    return response()->json([
        'message' => 'Logged out successfully'
    ])->withoutCookie('auth_token');
}

CORS Headers

php
public function handleCors(): ResponseInterface
{
    return response()->json($data)
        ->withHeaders([
            'Access-Control-Allow-Origin' => '*',
            'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE',
            'Access-Control-Allow-Headers' => 'Content-Type, Authorization',
            'Access-Control-Max-Age' => '86400'
        ]);
}

6.9 Response Helper Function

The response() helper function provides quick access to create HTTP responses throughout your application.

Basic Usage

Creating Response Instance

php
// Get response instance with status code
$response = response(200);

// Default status is 200
$response = response();

In Controllers

php
final readonly class UserController
{
    public function index(): ResponseInterface
    {
        $users = User::all();
        
        // Use helper to create JSON response
        return response()->json($users);
    }
    
    public function show(int $id): ResponseInterface
    {
        $user = User::find($id);
        
        if (!$user) {
            return response()->notFound('User not found');
        }
        
        return response()->ok($user);
    }
}

In Route Closures

php
use ElliePHP\Components\Routing\Router;

Router::get('/users', function() {
    $users = User::all();
    return response()->json($users);
});

Router::post('/users', function() {
    $user = User::create(request()->all());
    return response()->created($user);
});

Router::delete('/users/{id}', function($id) {
    User::delete($id);
    return response()->noContent();
});

Method Chaining

The helper enables fluent method chaining:

php
// Chain multiple methods
return response()
    ->json(['message' => 'Success'])
    ->withHeader('X-API-Version', '1.0')
    ->withStatus(201);

// Complex response
return response()
    ->json($data)
    ->withHeaders([
        'X-Total-Count' => count($data),
        'X-Request-ID' => $requestId
    ])
    ->etag($etag)
    ->cacheControl('public, max-age=300');

Usage Examples

API Endpoints

php
// Success response
return response()->json([
    'status' => 'success',
    'data' => $users
]);

// Error response
return response()->json([
    'status' => 'error',
    'message' => 'Validation failed',
    'errors' => $errors
], 422);

// With status helper
return response()->unprocessable([
    'errors' => $errors
]);

File Downloads

php
// Download file
return response()->file(
    path: storage_path('documents/report.pdf'),
    filename: 'monthly_report.pdf'
);

// Stream large file
return response()->streamDownload(
    path: $filePath,
    filename: 'backup.zip',
    deleteAfter: true
);

Redirects

php
// Redirect after form submission
return response()->redirect('/dashboard');

// Redirect back
return response()->back('/home');

// Permanent redirect
return response()->redirectPermanent('/new-url');

HTML Responses

php
// Render HTML
return response()->html('<h1>Welcome</h1>');

// With template
$html = view('welcome', ['name' => $name]);
return response()->html($html);

Singleton Behavior

The response() helper creates a new Response instance each time:

php
// Each call creates new instance
$response1 = response();
$response2 = response();

// $response1 !== $response2 (different instances)

This is different from request() which returns a singleton.

Complete Example

php
use Psr\Http\Message\ResponseInterface;

final readonly class ApiController
{
    public function __construct(
        private UserService $userService
    ) {}
    
    public function index(): ResponseInterface
    {
        $users = $this->userService->all();
        
        return response()
            ->json(['data' => $users])
            ->withHeaders([
                'X-Total-Count' => count($users),
                'X-API-Version' => '1.0'
            ]);
    }
    
    public function store(): ResponseInterface
    {
        $data = request()->all();
        
        if (!$this->validate($data)) {
            return response()->unprocessable([
                'errors' => $this->errors
            ]);
        }
        
        $user = $this->userService->create($data);
        
        return response()
            ->created($user)
            ->withHeader('Location', '/api/users/' . $user->id);
    }
    
    public function destroy(int $id): ResponseInterface
    {
        if (!$this->userService->exists($id)) {
            return response()->notFound('User not found');
        }
        
        $this->userService->delete($id);
        
        return response()->noContent();
    }
}

6.10 Response Inspection

The Response class provides methods to inspect response properties, useful for testing and debugging.

Status Code Inspection

Getting Status Code

php
// Get status code
$code = $response->status();
$code = $response->getStatusCode(); // Alias

// Example
$response = response()->notFound('User not found');
echo $response->status(); // 404

Status Code Checks

php
// Check if successful (2xx)
if ($response->isSuccessful()) {
    // Status code 200-299
}

// Alias
if ($response->successful()) {
    // Same as isSuccessful()
}

// Check if OK (200)
if ($response->isOk()) {
    // Status code is exactly 200
}

// Check if redirect (3xx)
if ($response->isRedirect()) {
    // Status code 300-399
}

// Check if client error (4xx)
if ($response->isClientError()) {
    // Status code 400-499
}

// Check if server error (5xx)
if ($response->isServerError()) {
    // Status code 500-599
}

Specific Status Checks

php
// Check if forbidden (403)
if ($response->isForbidden()) {
    // Status code is 403
}

// Check if not found (404)
if ($response->isNotFound()) {
    // Status code is 404
}

Body Content Inspection

Getting Body Content

php
// Get body as string
$content = $response->body();
$content = $response->content(); // Alias

// Example
$response = response()->json(['message' => 'Hello']);
echo $response->body(); // {"message":"Hello"}

JSON Encoding

php
// Get body as JSON string
$json = $response->toJson();

// Example
$response = response()->text('Hello World');
echo $response->toJson(); // "Hello World"

Header Inspection

Getting Headers

php
// Get all headers
$headers = $response->headers();

// Get specific header
$contentType = $response->getHeader('Content-Type');

// With default value
$customHeader = $response->getHeader('X-Custom', 'default');

// Check if header exists
if ($response->hasHeader('Content-Type')) {
    // Header exists
}

Header Examples

php
$response = response()->json(['data' => $users])
    ->withHeaders([
        'X-Total-Count' => '100',
        'X-Page' => '1'
    ]);

// Inspect headers
$totalCount = $response->getHeader('X-Total-Count'); // '100'
$page = $response->getHeader('X-Page'); // '1'
$allHeaders = $response->headers();

PSR-7 Access

Getting PSR-7 Response

php
// Get underlying PSR-7 response
$psrResponse = $response->psr();
$psrResponse = $response->raw(); // Alias

// Use PSR-7 methods
$statusCode = $psrResponse->getStatusCode();
$reasonPhrase = $psrResponse->getReasonPhrase();
$protocolVersion = $psrResponse->getProtocolVersion();

Testing Examples

PHPUnit Assertions

php
use PHPUnit\Framework\TestCase;

class UserControllerTest extends TestCase
{
    public function test_index_returns_successful_response()
    {
        $response = $this->controller->index();
        
        $this->assertTrue($response->isSuccessful());
        $this->assertEquals(200, $response->status());
        $this->assertTrue($response->hasHeader('Content-Type'));
    }
    
    public function test_show_returns_not_found_for_invalid_id()
    {
        $response = $this->controller->show(999);
        
        $this->assertTrue($response->isNotFound());
        $this->assertEquals(404, $response->status());
    }
    
    public function test_store_returns_created_with_location_header()
    {
        $response = $this->controller->store();
        
        $this->assertEquals(201, $response->status());
        $this->assertTrue($response->hasHeader('Location'));
        
        $body = json_decode($response->body(), true);
        $this->assertArrayHasKey('id', $body);
    }
    
    public function test_destroy_returns_no_content()
    {
        $response = $this->controller->destroy(1);
        
        $this->assertEquals(204, $response->status());
        $this->assertEmpty($response->body());
    }
}

Debugging Responses

php
// Debug response in development
$response = response()->json($data);

// Inspect response
var_dump([
    'status' => $response->status(),
    'successful' => $response->isSuccessful(),
    'headers' => $response->headers(),
    'body' => $response->body()
]);

// Check specific conditions
if ($response->isClientError()) {
    report()->error('Client error response', [
        'status' => $response->status(),
        'body' => $response->body()
    ]);
}

Complete Inspection Example

php
function inspectResponse(Response $response): array
{
    return [
        'status' => [
            'code' => $response->status(),
            'successful' => $response->isSuccessful(),
            'ok' => $response->isOk(),
            'redirect' => $response->isRedirect(),
            'client_error' => $response->isClientError(),
            'server_error' => $response->isServerError(),
        ],
        'headers' => $response->headers(),
        'body' => [
            'content' => $response->body(),
            'length' => strlen($response->body()),
            'json' => $response->toJson()
        ],
        'checks' => [
            'has_content_type' => $response->hasHeader('Content-Type'),
            'content_type' => $response->getHeader('Content-Type'),
            'is_json' => str_contains(
                $response->getHeader('Content-Type', ''),
                'application/json'
            )
        ]
    ];
}

// Usage
$response = response()->json(['message' => 'Success']);
$inspection = inspectResponse($response);
print_r($inspection);

Middleware Response Inspection

php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final readonly class ResponseLoggerMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $response = $handler->handle($request);
        
        // Wrap in Response for inspection
        $wrappedResponse = new Response($response);
        
        // Log response details
        report()->info('Response sent', [
            'status' => $wrappedResponse->status(),
            'successful' => $wrappedResponse->isSuccessful(),
            'content_type' => $wrappedResponse->getHeader('Content-Type'),
            'body_size' => strlen($wrappedResponse->body())
        ]);
        
        return $response;
    }
}

[Content to be added]


7. Caching

7.1 Cache Drivers Overview

ElliePHP provides a flexible, PSR-16 compliant caching system with support for multiple storage backends. The caching component allows you to store and retrieve data efficiently, reducing database queries and improving application performance.

PSR-16 Simple Cache Compliance

ElliePHP's cache implementation follows the PSR-16 Simple Cache Interface standard, which provides a straightforward API for caching operations. This compliance ensures:

  • Interoperability: Works with any PSR-16 compatible library or tool
  • Standardized API: Consistent method signatures across all drivers
  • Predictable Behavior: Well-defined behavior for edge cases and errors
  • Easy Testing: Simple interface makes mocking and testing straightforward

The PSR-16 interface defines these core methods:

php
interface CacheInterface
{
    public function get(string $key, mixed $default = null): mixed;
    public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool;
    public function delete(string $key): bool;
    public function clear(): bool;
    public function getMultiple(iterable $keys, mixed $default = null): iterable;
    public function setMultiple(iterable $values, null|int|\DateInterval $ttl = null): bool;
    public function deleteMultiple(iterable $keys): bool;
    public function has(string $key): bool;
}

Supported Cache Drivers

ElliePHP supports four cache drivers, each optimized for different use cases:

1. File Cache Driver (Default)

Stores cache data as JSON files on the filesystem.

Best for:

  • Development environments
  • Simple deployments without external dependencies
  • Applications with moderate caching needs
  • Shared hosting environments

Pros:

  • No external dependencies required
  • Easy to debug (human-readable JSON files)
  • Works on any system with file access
  • Simple setup and configuration

Cons:

  • Slower than memory-based caches
  • Not suitable for high-concurrency scenarios
  • Requires periodic garbage collection
  • File I/O overhead

2. Redis Cache Driver

Uses Redis server for high-performance in-memory caching.

Best for:

  • Production applications with high traffic
  • Distributed systems requiring shared cache
  • Applications needing fast read/write operations
  • Microservices architectures

Pros:

  • Extremely fast (in-memory storage)
  • Supports distributed caching across servers
  • Built-in expiration handling
  • Excellent for high-concurrency scenarios
  • Rich feature set (pub/sub, transactions, etc.)

Cons:

  • Requires Redis server installation and maintenance
  • Additional infrastructure complexity
  • Memory-based (data lost on restart without persistence)
  • Network latency for remote Redis servers

3. SQLite Cache Driver

Stores cache data in a SQLite database file.

Best for:

  • Applications already using SQLite
  • Moderate traffic applications
  • Scenarios requiring persistent cache
  • Single-server deployments

Pros:

  • Persistent storage (survives restarts)
  • ACID compliance for data integrity
  • No external server required
  • Good performance for moderate loads
  • Supports complex queries if needed

Cons:

  • Slower than memory-based caches
  • Write operations can be bottleneck under high load
  • Requires periodic garbage collection
  • Single-file locking can limit concurrency

4. APCu Cache Driver

Uses PHP's APCu extension for shared memory caching.

Best for:

  • Single-server deployments
  • Applications needing fastest possible cache
  • Scenarios where Redis is overkill
  • Caching configuration and compiled data

Pros:

  • Fastest option (shared memory)
  • No network overhead
  • No external dependencies (just PHP extension)
  • Automatic memory management

Cons:

  • Single-server only (not shared across servers)
  • Requires APCu PHP extension
  • Cache cleared on PHP-FPM/Apache restart
  • Limited by available PHP memory

Choosing the Right Driver

Use this decision tree to select the appropriate cache driver:

Do you need shared cache across multiple servers?
├─ Yes → Use Redis
└─ No → Continue

Is this a production environment with high traffic?
├─ Yes → Use Redis or APCu
└─ No → Continue

Do you need persistent cache (survives restarts)?
├─ Yes → Use SQLite or File
└─ No → Use APCu

Is APCu extension available?
├─ Yes → Use APCu (fastest)
└─ No → Use File (simplest)

Quick Recommendations:

  • Development: File driver (default, no setup required)
  • Production (single server): APCu driver (fastest)
  • Production (multiple servers): Redis driver (shared cache)
  • Moderate traffic: SQLite driver (good balance)
  • Shared hosting: File driver (most compatible)

Driver Configuration

Configure your cache driver in the .env file:

env
# Cache Configuration
CACHE_DRIVER=file

# Redis Configuration (when using redis driver)
REDIS_HOST='127.0.0.1'
REDIS_PORT=6379
REDIS_PASSWORD=null
REDIS_DATABASE=0
REDIS_TIMEOUT=5

The framework automatically selects and configures the appropriate driver based on your CACHE_DRIVER setting.

7.2 File Cache Driver

The File cache driver stores cache data as JSON files on the filesystem. It's the default driver and requires no external dependencies, making it ideal for development and simple deployments.

Configuration

The File driver is configured using CacheFactory::createFileDriver() with the following options:

php
use ElliePHP\Components\Cache\CacheFactory;
use ElliePHP\Components\Cache\Cache;

$driver = CacheFactory::createFileDriver([
    'path' => storage_cache_path(),           // Cache directory path
    'create_directory' => true,                // Auto-create directory if missing
    'directory_permissions' => 0755            // Directory permissions (octal)
]);

$cache = new Cache($driver);

Configuration Options:

  • path (string, required): Absolute path to the cache directory where files will be stored
  • create_directory (bool, default: true): Automatically create the directory if it doesn't exist
  • directory_permissions (int, default: 0755): Unix permissions for the created directory (octal notation)

Default Configuration

When using the cache() helper function with CACHE_DRIVER=file, the framework uses these defaults:

php
// Automatically configured when using cache() helper
$cache = cache(); // or cache('file')

// Equivalent to:
$driver = CacheFactory::createFileDriver([
    'path' => storage_cache_path() // storage/Cache/ directory
]);
$cache = new Cache($driver);

How It Works

The File driver stores each cache entry as a separate JSON file:

File Structure:

storage/Cache/
├── ellie_cache_user_123.json
├── ellie_cache_config_settings.json
└── ellie_cache_api_response.json

File Content Example:

json
{
    "key": "ellie_cache:user:123",
    "value": {
        "id": 123,
        "name": "John Doe",
        "email": "john@example.com"
    },
    "expiry": 1700000000
}

Each file contains:

  • key: The cache key with automatic prefix
  • value: The cached data (serialized to JSON)
  • expiry: Unix timestamp when the entry expires (null for permanent)

Usage Examples

Basic Usage:

php
use function cache;

// Get cache instance with file driver
$cache = cache('file');

// Store data
$cache->set('user:123', [
    'id' => 123,
    'name' => 'John Doe',
    'email' => 'john@example.com'
], 3600); // Cache for 1 hour

// Retrieve data
$user = $cache->get('user:123');

// Check if exists
if ($cache->has('user:123')) {
    echo "User is cached";
}

// Delete entry
$cache->delete('user:123');

Custom Configuration:

php
use ElliePHP\Components\Cache\CacheFactory;
use ElliePHP\Components\Cache\Cache;

// Custom cache directory
$driver = CacheFactory::createFileDriver([
    'path' => '/var/cache/myapp',
    'create_directory' => true,
    'directory_permissions' => 0750 // More restrictive permissions
]);

$cache = new Cache($driver);

// Use the cache
$cache->set('config', $settings, 86400); // Cache for 24 hours

Caching Expensive Operations:

php
function getExpensiveData(int $id): array
{
    $cache = cache('file');
    $cacheKey = "expensive:data:{$id}";
    
    // Try to get from cache
    $data = $cache->get($cacheKey);
    
    if ($data === null) {
        // Cache miss - perform expensive operation
        $data = performExpensiveOperation($id);
        
        // Store in cache for 1 hour
        $cache->set($cacheKey, $data, 3600);
    }
    
    return $data;
}

Garbage Collection

The File driver requires periodic garbage collection to remove expired cache files. Expired files are not automatically deleted when they expire; they're only removed when accessed or during manual cleanup.

Automatic Cleanup:

The cache() helper function automatically calls clearExpired() when creating a File driver instance:

php
// Automatically clears expired entries
$cache = cache('file');

Manual Cleanup:

You can manually trigger garbage collection:

php
use ElliePHP\Components\Cache\CacheFactory;

$driver = CacheFactory::createFileDriver([
    'path' => storage_cache_path()
]);

// Clear all expired cache files
$deletedCount = $driver->clearExpired();

echo "Deleted {$deletedCount} expired cache files";

Scheduled Cleanup:

For production applications, schedule periodic cleanup using a cron job or console command:

php
// In a custom console command
public function handle(): int
{
    $driver = CacheFactory::createFileDriver([
        'path' => storage_cache_path()
    ]);
    
    $deleted = $driver->clearExpired();
    
    $this->success("Cleared {$deleted} expired cache entries");
    
    return self::SUCCESS;
}

Cron Job Example:

bash
# Run cleanup daily at 2 AM
0 2 * * * cd /path/to/app && php ellie cache:cleanup

Performance Considerations

Pros:

  • Simple and reliable
  • No external dependencies
  • Easy to debug (human-readable JSON)
  • Works on any system

Cons:

  • Slower than memory-based caches
  • File I/O overhead
  • Not ideal for high-concurrency scenarios
  • Requires periodic garbage collection

Optimization Tips:

  1. Use SSD Storage: Store cache files on SSD for faster I/O
  2. Limit Cache Size: Don't cache extremely large objects
  3. Set Appropriate TTLs: Shorter TTLs reduce disk usage
  4. Regular Cleanup: Schedule clearExpired() to prevent disk bloat
  5. Consider Alternatives: For high-traffic production, use Redis or APCu

File Permissions

Ensure the cache directory is writable by the web server:

bash
# Set ownership
sudo chown -R www-data:www-data storage/Cache

# Set permissions
chmod -R 775 storage/Cache

For security, ensure the cache directory is not publicly accessible via the web server.

Troubleshooting

Permission Denied Errors:

php
// Ensure directory is writable
if (!is_writable(storage_cache_path())) {
    throw new Exception('Cache directory is not writable');
}

Disk Space Issues:

php
// Check available disk space
$cache = cache('file');
$size = $cache->size(); // Total cache size in bytes

if ($size > 100 * 1024 * 1024) { // 100MB
    $cache->clear(); // Clear all cache
}

Debugging Cache Files:

bash
# List cache files
ls -lh storage/Cache/

# View cache file content
cat storage/Cache/ellie_cache_user_123.json | jq .

# Count cache files
find storage/Cache -name "*.json" | wc -l

7.3 Redis Cache Driver

The Redis cache driver provides high-performance, in-memory caching using Redis server. It's the recommended choice for production applications with high traffic and distributed systems requiring shared cache across multiple servers.

Prerequisites

Before using the Redis driver, ensure:

  1. Redis Server: Redis 5.0+ installed and running
  2. PHP Extension: ext-redis PHP extension installed
  3. Network Access: Application can connect to Redis server

Install Redis Extension:

bash
# Using PECL
pecl install redis

# Enable in php.ini
extension=redis.so

Verify Installation:

bash
php -m | grep redis
# Should output: redis

Configuration

The Redis driver is configured using CacheFactory::createRedisDriver() with connection options:

php
use ElliePHP\Components\Cache\CacheFactory;
use ElliePHP\Components\Cache\Cache;

$driver = CacheFactory::createRedisDriver([
    'host' => '127.0.0.1',        // Redis server hostname or IP
    'port' => 6379,                // Redis server port
    'password' => null,            // Authentication password (optional)
    'database' => 0,               // Redis database number (0-15)
    'timeout' => 5.0,              // Connection timeout in seconds
    'prefix' => 'myapp:'           // Optional key prefix
]);

$cache = new Cache($driver);

Configuration Options:

  • host (string, default: '127.0.0.1'): Redis server hostname or IP address
  • port (int, default: 6379): Redis server port number
  • password (string|null, default: null): Authentication password for Redis server
  • database (int, default: 0): Redis database number to use (0-15)
  • timeout (float, default: 5.0): Connection timeout in seconds
  • prefix (string, default: ''): Optional prefix for all cache keys

Environment Configuration

Configure Redis connection in your .env file:

env
# Cache Configuration
CACHE_DRIVER=redis

# Redis Configuration
REDIS_HOST='127.0.0.1'
REDIS_PORT=6379
REDIS_PASSWORD=null
REDIS_DATABASE=0
REDIS_TIMEOUT=5

When using the cache() helper with CACHE_DRIVER=redis, these environment variables are automatically used:

php
// Automatically configured from .env
$cache = cache('redis');

// Equivalent to:
$driver = CacheFactory::createRedisDriver([
    'host' => env('REDIS_HOST', '127.0.0.1'),
    'port' => env('REDIS_PORT', 6379),
    'password' => env('REDIS_PASSWORD'),
    'database' => env('REDIS_DATABASE', 0),
    'timeout' => env('REDIS_TIMEOUT', 5),
]);
$cache = new Cache($driver);

Connection Handling

The Redis driver automatically manages connections:

Automatic Connection:

php
$cache = cache('redis');

// Connection is established on first operation
$cache->set('key', 'value'); // Connects to Redis here

Connection Pooling:

The driver reuses connections within the same request, avoiding connection overhead:

php
$cache1 = cache('redis');
$cache2 = cache('redis');

// Both instances share the same Redis connection
$cache1->set('key1', 'value1');
$cache2->set('key2', 'value2');

Connection Errors:

php
use ElliePHP\Components\Cache\Exceptions\CacheException;

try {
    $driver = CacheFactory::createRedisDriver([
        'host' => 'invalid-host',
        'timeout' => 2
    ]);
    
    $cache = new Cache($driver);
    $cache->set('key', 'value');
    
} catch (CacheException $e) {
    // Handle connection failure
    report()->error('Redis connection failed', [
        'error' => $e->getMessage()
    ]);
    
    // Fallback to file cache
    $cache = cache('file');
}

Usage Examples

Basic Operations:

php
use function cache;

// Get Redis cache instance
$cache = cache('redis');

// Store data
$cache->set('user:123', [
    'id' => 123,
    'name' => 'John Doe',
    'email' => 'john@example.com'
], 3600); // Cache for 1 hour

// Retrieve data
$user = $cache->get('user:123');

// Check existence
if ($cache->has('user:123')) {
    echo "User is cached in Redis";
}

// Delete entry
$cache->delete('user:123');

Session Storage:

php
// Store user session in Redis
$sessionId = session_id();
$cache = cache('redis');

$cache->set("session:{$sessionId}", [
    'user_id' => 123,
    'logged_in_at' => time(),
    'ip_address' => $_SERVER['REMOTE_ADDR']
], 1800); // 30 minutes

// Retrieve session
$session = $cache->get("session:{$sessionId}");

API Response Caching:

php
function fetchApiData(string $endpoint): array
{
    $cache = cache('redis');
    $cacheKey = "api:response:{$endpoint}";
    
    // Try cache first
    $data = $cache->get($cacheKey);
    
    if ($data === null) {
        // Cache miss - fetch from API
        $data = file_get_contents("https://api.example.com/{$endpoint}");
        $data = json_decode($data, true);
        
        // Cache for 5 minutes
        $cache->set($cacheKey, $data, 300);
    }
    
    return $data;
}

Rate Limiting:

php
function checkRateLimit(string $userId): bool
{
    $cache = cache('redis');
    $key = "rate_limit:{$userId}";
    
    $requests = (int) $cache->get($key, 0);
    
    if ($requests >= 100) {
        return false; // Rate limit exceeded
    }
    
    // Increment counter
    $cache->set($key, $requests + 1, 3600); // Reset after 1 hour
    
    return true;
}

Distributed Locking:

php
function acquireLock(string $resource, int $ttl = 10): bool
{
    $cache = cache('redis');
    $lockKey = "lock:{$resource}";
    
    // Try to acquire lock
    if ($cache->has($lockKey)) {
        return false; // Lock already held
    }
    
    // Set lock with TTL
    $cache->set($lockKey, true, $ttl);
    return true;
}

function releaseLock(string $resource): void
{
    $cache = cache('redis');
    $cache->delete("lock:{$resource}");
}

// Usage
if (acquireLock('user:123:update')) {
    try {
        // Perform critical operation
        updateUser(123, $data);
    } finally {
        releaseLock('user:123:update');
    }
}

Multiple Redis Databases

Redis supports 16 databases (0-15). Use different databases to isolate cache data:

php
// Cache database (database 0)
$cacheDriver = CacheFactory::createRedisDriver([
    'host' => '127.0.0.1',
    'database' => 0
]);
$cache = new Cache($cacheDriver);

// Session database (database 1)
$sessionDriver = CacheFactory::createRedisDriver([
    'host' => '127.0.0.1',
    'database' => 1
]);
$sessionCache = new Cache($sessionDriver);

// Queue database (database 2)
$queueDriver = CacheFactory::createRedisDriver([
    'host' => '127.0.0.1',
    'database' => 2
]);
$queueCache = new Cache($queueDriver);

Key Prefixing

Use key prefixes to namespace your cache entries:

php
// Production cache
$prodDriver = CacheFactory::createRedisDriver([
    'host' => '127.0.0.1',
    'prefix' => 'prod:'
]);

// Staging cache (same Redis server)
$stagingDriver = CacheFactory::createRedisDriver([
    'host' => '127.0.0.1',
    'prefix' => 'staging:'
]);

// Keys are automatically prefixed
$prodCache = new Cache($prodDriver);
$prodCache->set('user:123', $data); // Stored as "prod:ellie_cache:user:123"

$stagingCache = new Cache($stagingDriver);
$stagingCache->set('user:123', $data); // Stored as "staging:ellie_cache:user:123"

Performance Characteristics

Advantages:

  • Extremely Fast: In-memory storage with microsecond latency
  • High Throughput: Handles thousands of operations per second
  • Distributed: Share cache across multiple application servers
  • Automatic Expiration: Built-in TTL handling, no garbage collection needed
  • Atomic Operations: Thread-safe operations for concurrent access
  • Persistence Options: Optional disk persistence for durability

Benchmarks:

Operation          | Operations/sec | Latency
-------------------|----------------|----------
SET (simple)       | ~100,000       | ~10μs
GET (simple)       | ~100,000       | ~10μs
SET (complex obj)  | ~50,000        | ~20μs
GET (complex obj)  | ~50,000        | ~20μs

Production Best Practices

1. Connection Pooling:

php
// Reuse cache instances
class CacheManager
{
    private static ?Cache $instance = null;
    
    public static function getInstance(): Cache
    {
        if (self::$instance === null) {
            self::$instance = cache('redis');
        }
        
        return self::$instance;
    }
}

2. Graceful Degradation:

php
function getCachedData(string $key, callable $fallback): mixed
{
    try {
        $cache = cache('redis');
        
        $data = $cache->get($key);
        
        if ($data === null) {
            $data = $fallback();
            $cache->set($key, $data, 3600);
        }
        
        return $data;
        
    } catch (CacheException $e) {
        // Redis unavailable - fetch directly
        report()->warning('Redis cache unavailable', [
            'error' => $e->getMessage()
        ]);
        
        return $fallback();
    }
}

3. Monitor Redis Health:

php
function checkRedisHealth(): bool
{
    try {
        $cache = cache('redis');
        $cache->set('health_check', true, 10);
        
        return $cache->get('health_check') === true;
        
    } catch (CacheException $e) {
        return false;
    }
}

4. Set Appropriate TTLs:

php
// Short-lived data (5 minutes)
$cache->set('api:rate_limit', $data, 300);

// Medium-lived data (1 hour)
$cache->set('user:session', $data, 3600);

// Long-lived data (24 hours)
$cache->set('config:settings', $data, 86400);

Troubleshooting

Connection Refused:

bash
# Check if Redis is running
redis-cli ping
# Should return: PONG

# Check Redis status
sudo systemctl status redis

# Start Redis
sudo systemctl start redis

Authentication Errors:

env
# Set password in .env
REDIS_PASSWORD='your-secure-password'
bash
# Set password in Redis config
# Edit /etc/redis/redis.conf
requirepass your-secure-password

# Restart Redis
sudo systemctl restart redis

Memory Issues:

bash
# Check Redis memory usage
redis-cli info memory

# Set max memory in redis.conf
maxmemory 256mb
maxmemory-policy allkeys-lru

# Restart Redis
sudo systemctl restart redis

Network Latency:

php
// Use shorter timeout for local Redis
$driver = CacheFactory::createRedisDriver([
    'host' => '127.0.0.1',
    'timeout' => 1.0 // 1 second for local
]);

// Use longer timeout for remote Redis
$driver = CacheFactory::createRedisDriver([
    'host' => 'redis.example.com',
    'timeout' => 5.0 // 5 seconds for remote
]);

7.4 SQLite Cache Driver

The SQLite cache driver stores cache data in a SQLite database file, providing persistent storage with ACID compliance. It's a good middle-ground option for applications that need persistent cache without the complexity of Redis.

Prerequisites

SQLite support is included in PHP by default. Ensure these extensions are enabled:

  • ext-pdo (required)
  • ext-pdo_sqlite (required)

Verify Installation:

bash
php -m | grep -E "pdo|sqlite"
# Should output: PDO, pdo_sqlite

These extensions are typically enabled by default in PHP 8.4+.

Configuration

The SQLite driver is configured using CacheFactory::createSQLiteDriver():

php
use ElliePHP\Components\Cache\CacheFactory;
use ElliePHP\Components\Cache\Cache;

$driver = CacheFactory::createSQLiteDriver([
    'path' => storage_cache_path('cache.db'),  // Database file path
    'create_directory' => true,                 // Auto-create directory
    'directory_permissions' => 0755             // Directory permissions
]);

$cache = new Cache($driver);

Configuration Options:

  • path (string, required): Absolute path to the SQLite database file
  • create_directory (bool, default: true): Automatically create parent directory if missing
  • directory_permissions (int, default: 0755): Unix permissions for created directory

Default Configuration

When using the cache() helper with CACHE_DRIVER=sqlite:

php
// Automatically configured
$cache = cache('sqlite');

// Equivalent to:
$driver = CacheFactory::createSQLiteDriver([
    'path' => storage_cache_path('cache.db'),
    'create_directory' => true,
    'directory_permissions' => 0755,
]);
$cache = new Cache($driver);

The database file is stored at storage/Cache/cache.db by default.

Database Structure

The SQLite driver automatically creates a table to store cache entries:

sql
CREATE TABLE IF NOT EXISTS cache (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL,
    expiry INTEGER
);

CREATE INDEX IF NOT EXISTS idx_expiry ON cache(expiry);

Table Schema:

  • key: Cache key (primary key, unique)
  • value: Serialized cache value (JSON)
  • expiry: Unix timestamp when entry expires (NULL for permanent)

The expiry index improves performance when clearing expired entries.

Usage Examples

Basic Operations:

php
use function cache;

// Get SQLite cache instance
$cache = cache('sqlite');

// Store data
$cache->set('user:123', [
    'id' => 123,
    'name' => 'John Doe',
    'email' => 'john@example.com'
], 3600); // Cache for 1 hour

// Retrieve data
$user = $cache->get('user:123');

// Check existence
if ($cache->has('user:123')) {
    echo "User is cached in SQLite";
}

// Delete entry
$cache->delete('user:123');

Persistent Configuration Cache:

php
// Cache application configuration
function getConfig(string $key): mixed
{
    $cache = cache('sqlite');
    $cacheKey = "config:{$key}";
    
    $value = $cache->get($cacheKey);
    
    if ($value === null) {
        // Load from config file
        $value = config($key);
        
        // Cache permanently (survives restarts)
        $cache->set($cacheKey, $value, null);
    }
    
    return $value;
}

Database Query Caching:

php
function getCachedQuery(string $sql, array $params = []): array
{
    $cache = cache('sqlite');
    $cacheKey = 'query:' . md5($sql . serialize($params));
    
    $result = $cache->get($cacheKey);
    
    if ($result === null) {
        // Execute query
        $result = $database->query($sql, $params);
        
        // Cache for 10 minutes
        $cache->set($cacheKey, $result, 600);
    }
    
    return $result;
}

Custom Database Location:

php
use ElliePHP\Components\Cache\CacheFactory;
use ElliePHP\Components\Cache\Cache;

// Use custom database location
$driver = CacheFactory::createSQLiteDriver([
    'path' => '/var/cache/myapp/cache.db',
    'create_directory' => true,
    'directory_permissions' => 0750
]);

$cache = new Cache($driver);

// Use the cache
$cache->set('data', $value, 3600);

Garbage Collection

Like the File driver, SQLite requires periodic garbage collection to remove expired entries:

Automatic Cleanup:

The cache() helper automatically calls clearExpired() when creating a SQLite driver:

php
// Automatically clears expired entries
$cache = cache('sqlite');

Manual Cleanup:

php
use ElliePHP\Components\Cache\CacheFactory;

$driver = CacheFactory::createSQLiteDriver([
    'path' => storage_cache_path('cache.db')
]);

// Clear all expired entries
$deletedCount = $driver->clearExpired();

echo "Deleted {$deletedCount} expired cache entries";

Scheduled Cleanup:

Create a console command for periodic cleanup:

php
// In a custom console command
public function handle(): int
{
    $driver = CacheFactory::createSQLiteDriver([
        'path' => storage_cache_path('cache.db')
    ]);
    
    $deleted = $driver->clearExpired();
    
    $this->success("Cleared {$deleted} expired cache entries from SQLite");
    
    // Optimize database
    $this->info("Optimizing database...");
    // SQLite VACUUM command to reclaim space
    
    return self::SUCCESS;
}

Cron Job:

bash
# Run cleanup daily at 3 AM
0 3 * * * cd /path/to/app && php ellie cache:cleanup

Performance Characteristics

Advantages:

  • Persistent Storage: Cache survives application restarts
  • ACID Compliance: Data integrity guaranteed
  • No External Server: Self-contained database file
  • Good Performance: Faster than file cache for most operations
  • Concurrent Reads: Multiple processes can read simultaneously
  • SQL Queries: Can query cache data if needed

Limitations:

  • Write Bottleneck: Single-file locking limits concurrent writes
  • Slower than Memory: Not as fast as Redis or APCu
  • Disk I/O: Performance depends on disk speed
  • Single Server: Not suitable for distributed systems

Benchmarks:

Operation          | Operations/sec | Latency
-------------------|----------------|----------
SET (simple)       | ~5,000         | ~200μs
GET (simple)       | ~10,000        | ~100μs
SET (complex obj)  | ~3,000         | ~300μs
GET (complex obj)  | ~8,000         | ~125μs

Best Practices

1. Use SSD Storage:

php
// Store database on fast SSD
$driver = CacheFactory::createSQLiteDriver([
    'path' => '/mnt/ssd/cache/cache.db'
]);

2. Regular Maintenance:

php
// Periodically vacuum database to reclaim space
function optimizeCacheDatabase(): void
{
    $dbPath = storage_cache_path('cache.db');
    $pdo = new PDO("sqlite:{$dbPath}");
    
    // Reclaim unused space
    $pdo->exec('VACUUM');
    
    // Analyze for query optimization
    $pdo->exec('ANALYZE');
}

3. Monitor Database Size:

php
function getCacheDatabaseSize(): int
{
    $dbPath = storage_cache_path('cache.db');
    
    if (file_exists($dbPath)) {
        return filesize($dbPath);
    }
    
    return 0;
}

// Check size
$size = getCacheDatabaseSize();
$sizeMB = round($size / 1024 / 1024, 2);

if ($sizeMB > 100) {
    // Database too large - clear old entries
    cache('sqlite')->clear();
}

4. Set Appropriate TTLs:

php
// Short-lived data (5 minutes)
$cache->set('temp:data', $value, 300);

// Medium-lived data (1 hour)
$cache->set('user:session', $value, 3600);

// Long-lived data (7 days)
$cache->set('static:content', $value, 604800);

// Permanent data (until manually deleted)
$cache->set('app:version', $value, null);

5. Backup Strategy:

bash
# Backup cache database
cp storage/Cache/cache.db storage/Cache/cache.db.backup

# Or use SQLite backup command
sqlite3 storage/Cache/cache.db ".backup storage/Cache/cache.db.backup"

Troubleshooting

Database Locked Errors:

SQLite uses file-level locking. If you encounter "database is locked" errors:

php
// Reduce concurrent writes
// Use shorter TTLs to reduce write frequency
// Consider Redis for high-concurrency scenarios

// Or implement retry logic
function setCacheWithRetry(string $key, mixed $value, int $ttl): bool
{
    $cache = cache('sqlite');
    $attempts = 0;
    $maxAttempts = 3;
    
    while ($attempts < $maxAttempts) {
        try {
            return $cache->set($key, $value, $ttl);
        } catch (CacheException $e) {
            $attempts++;
            usleep(100000); // Wait 100ms before retry
        }
    }
    
    return false;
}

Permission Errors:

bash
# Ensure database file is writable
chmod 664 storage/Cache/cache.db

# Ensure directory is writable
chmod 775 storage/Cache

# Set correct ownership
chown www-data:www-data storage/Cache/cache.db

Database Corruption:

bash
# Check database integrity
sqlite3 storage/Cache/cache.db "PRAGMA integrity_check;"

# If corrupted, rebuild
rm storage/Cache/cache.db
# Database will be recreated on next cache operation

Large Database Size:

php
// Clear all cache
cache('sqlite')->clear();

// Then vacuum to reclaim space
$dbPath = storage_cache_path('cache.db');
$pdo = new PDO("sqlite:{$dbPath}");
$pdo->exec('VACUUM');

When to Use SQLite Cache

Good For:

  • Applications needing persistent cache
  • Moderate traffic applications (< 1000 req/min)
  • Single-server deployments
  • Development and staging environments
  • Applications already using SQLite

Not Ideal For:

  • High-concurrency applications (use Redis)
  • Distributed systems (use Redis)
  • Applications requiring fastest possible cache (use APCu)
  • Very high write volumes (use Redis)

Comparison with Other Drivers

Feature              | SQLite | File  | Redis | APCu
---------------------|--------|-------|-------|------
Persistent           | Yes    | Yes   | No*   | No
Performance          | Good   | Fair  | Best  | Best
Concurrent Writes    | Fair   | Fair  | Best  | Good
Distributed          | No     | No    | Yes   | No
External Dependency  | No     | No    | Yes   | No
Setup Complexity     | Low    | Low   | Med   | Low
ACID Compliance      | Yes    | No    | No    | No

* Redis can be configured for persistence

7.5 APCu Cache Driver

The APCu (Alternative PHP Cache - User Cache) driver provides the fastest caching option by storing data in shared memory. It's ideal for single-server production deployments where maximum performance is required.

What is APCu?

APCu is a PHP extension that provides shared memory storage for user data. Unlike opcode caches (like OPcache), APCu is specifically designed for caching application data.

Key Characteristics:

  • In-Memory Storage: Data stored in RAM for microsecond access times
  • Shared Memory: Cache shared across all PHP processes on the same server
  • Process-Persistent: Cache survives between requests but not server restarts
  • Zero Network Overhead: No network latency like Redis
  • Automatic Memory Management: Built-in eviction policies

Prerequisites

APCu requires the ext-apcu PHP extension to be installed and enabled.

Install APCu Extension:

bash
# Using PECL
pecl install apcu

# On Ubuntu/Debian
sudo apt-get install php8.4-apcu

# On macOS with Homebrew
brew install php@8.4
pecl install apcu

Enable in php.ini:

ini
extension=apcu.so
apc.enabled=1
apc.shm_size=32M
apc.ttl=7200
apc.enable_cli=1

Verify Installation:

bash
php -m | grep apcu
# Should output: apcu

# Check APCu info
php -r "var_dump(apcu_cache_info());"

Configuration

The APCu driver has minimal configuration since it uses shared memory:

php
use ElliePHP\Components\Cache\CacheFactory;
use ElliePHP\Components\Cache\Cache;

$driver = CacheFactory::createApcuDriver();
$cache = new Cache($driver);

No Configuration Options:

Unlike other drivers, APCu doesn't require connection parameters. Configuration is done in php.ini:

ini
# APCu Configuration in php.ini
apc.enabled=1              # Enable APCu
apc.shm_size=32M          # Shared memory size (32MB)
apc.ttl=7200              # Default TTL in seconds
apc.gc_ttl=3600           # Garbage collection TTL
apc.enable_cli=1          # Enable for CLI (useful for testing)
apc.serializer=php        # Serializer (php, igbinary, msgpack)

Using APCu Cache

Basic Usage:

php
use function cache;

// Get APCu cache instance
$cache = cache('apcu');

// Store data
$cache->set('user:123', [
    'id' => 123,
    'name' => 'John Doe',
    'email' => 'john@example.com'
], 3600); // Cache for 1 hour

// Retrieve data
$user = $cache->get('user:123');

// Check existence
if ($cache->has('user:123')) {
    echo "User is cached in APCu";
}

// Delete entry
$cache->delete('user:123');

Environment Configuration:

env
# Use APCu in production
CACHE_DRIVER=apcu
php
// Automatically uses APCu
$cache = cache(); // Uses CACHE_DRIVER from .env

In-Memory Caching Benefits

1. Extreme Performance:

APCu is the fastest caching option available:

Operation          | Operations/sec | Latency
-------------------|----------------|----------
SET (simple)       | ~200,000       | ~5μs
GET (simple)       | ~250,000       | ~4μs
SET (complex obj)  | ~100,000       | ~10μs
GET (complex obj)  | ~150,000       | ~7μs

2. Zero Network Overhead:

Unlike Redis, APCu has no network latency:

php
// APCu: Direct memory access (~5μs)
$cache = cache('apcu');
$value = $cache->get('key');

// Redis: Network round-trip (~100μs local, ~1-10ms remote)
$cache = cache('redis');
$value = $cache->get('key');

3. Automatic Memory Management:

APCu automatically manages memory with LRU (Least Recently Used) eviction:

php
// When memory is full, APCu automatically evicts least-used entries
$cache = cache('apcu');

// No manual cleanup needed
for ($i = 0; $i < 10000; $i++) {
    $cache->set("key:{$i}", $data, 3600);
}
// Oldest/least-used entries automatically removed when memory full

4. Process-Shared Cache:

Cache is shared across all PHP processes (PHP-FPM workers, Apache processes):

php
// Process 1
$cache = cache('apcu');
$cache->set('shared:data', 'value');

// Process 2 (different PHP-FPM worker)
$cache = cache('apcu');
$value = $cache->get('shared:data'); // Returns 'value'

Usage Examples

Configuration Caching:

php
// Cache compiled configuration in APCu
function getCompiledConfig(): array
{
    $cache = cache('apcu');
    $cacheKey = 'config:compiled';
    
    $config = $cache->get($cacheKey);
    
    if ($config === null) {
        // Load and compile all config files
        $config = compileAllConfigs();
        
        // Cache permanently (until server restart)
        $cache->set($cacheKey, $config, null);
    }
    
    return $config;
}

Route Caching:

php
// Cache compiled routes
function getCachedRoutes(): array
{
    $cache = cache('apcu');
    $cacheKey = 'routes:compiled';
    
    $routes = $cache->get($cacheKey);
    
    if ($routes === null) {
        // Compile routes
        $routes = compileRoutes();
        
        // Cache until deployment
        $cache->set($cacheKey, $routes, null);
    }
    
    return $routes;
}

Computed Values:

php
// Cache expensive computations
function getStatistics(): array
{
    $cache = cache('apcu');
    $cacheKey = 'stats:dashboard';
    
    $stats = $cache->get($cacheKey);
    
    if ($stats === null) {
        // Expensive calculation
        $stats = [
            'total_users' => countUsers(),
            'active_sessions' => countActiveSessions(),
            'revenue_today' => calculateRevenue(),
        ];
        
        // Cache for 5 minutes
        $cache->set($cacheKey, $stats, 300);
    }
    
    return $stats;
}

Template Compilation:

php
// Cache compiled templates
function getCompiledTemplate(string $template): string
{
    $cache = cache('apcu');
    $cacheKey = "template:{$template}";
    
    $compiled = $cache->get($cacheKey);
    
    if ($compiled === null) {
        // Compile template
        $compiled = compileTemplate($template);
        
        // Cache permanently
        $cache->set($cacheKey, $compiled, null);
    }
    
    return $compiled;
}

Memory Management

Check Memory Usage:

php
// Get APCu cache info
$info = apcu_cache_info();

echo "Memory Size: " . $info['mem_size'] . " bytes\n";
echo "Available Memory: " . $info['avail_mem'] . " bytes\n";
echo "Cached Entries: " . $info['num_entries'] . "\n";
echo "Cache Hits: " . $info['num_hits'] . "\n";
echo "Cache Misses: " . $info['num_misses'] . "\n";

Monitor Cache Efficiency:

php
function getCacheHitRate(): float
{
    $info = apcu_cache_info();
    
    $hits = $info['num_hits'];
    $misses = $info['num_misses'];
    $total = $hits + $misses;
    
    if ($total === 0) {
        return 0.0;
    }
    
    return ($hits / $total) * 100;
}

$hitRate = getCacheHitRate();
echo "Cache hit rate: " . round($hitRate, 2) . "%\n";

Clear APCu Cache:

php
// Clear all cache entries
$cache = cache('apcu');
$cache->clear();

// Or use APCu directly
apcu_clear_cache();

PHP Extension Requirement

APCu is a PHP extension that must be installed separately. If the extension is not available:

Check Availability:

php
if (!extension_loaded('apcu')) {
    throw new Exception('APCu extension is not installed');
}

if (!ini_get('apc.enabled')) {
    throw new Exception('APCu is not enabled in php.ini');
}

Graceful Fallback:

php
function getCacheDriver(): Cache
{
    // Try APCu first
    if (extension_loaded('apcu') && ini_get('apc.enabled')) {
        return cache('apcu');
    }
    
    // Fallback to file cache
    report()->warning('APCu not available, using file cache');
    return cache('file');
}

Best Practices

1. Use for Frequently Accessed Data:

php
// Good: Frequently accessed configuration
$cache->set('config:app', $config, null);

// Good: Compiled routes
$cache->set('routes:compiled', $routes, null);

// Bad: User-specific data (use Redis for distributed)
$cache->set('user:123:cart', $cart, 3600);

2. Set Appropriate Memory Size:

ini
# In php.ini
# Small application (< 1000 users)
apc.shm_size=32M

# Medium application (1000-10000 users)
apc.shm_size=64M

# Large application (> 10000 users)
apc.shm_size=128M

3. Monitor Memory Usage:

php
// Create monitoring endpoint
function getCacheStats(): array
{
    $info = apcu_cache_info();
    
    return [
        'memory_total' => $info['mem_size'],
        'memory_used' => $info['mem_size'] - $info['avail_mem'],
        'memory_available' => $info['avail_mem'],
        'entries' => $info['num_entries'],
        'hits' => $info['num_hits'],
        'misses' => $info['num_misses'],
        'hit_rate' => ($info['num_hits'] / ($info['num_hits'] + $info['num_misses'])) * 100
    ];
}

4. Clear Cache on Deployment:

bash
# In deployment script
php -r "apcu_clear_cache();"

# Or via console command
php ellie cache:clear --all

Limitations

Single-Server Only:

APCu cache is not shared across multiple servers:

php
// Server 1
$cache = cache('apcu');
$cache->set('data', 'value');

// Server 2 (different physical server)
$cache = cache('apcu');
$value = $cache->get('data'); // Returns null (not shared)

For distributed systems, use Redis instead.

Cache Cleared on Restart:

APCu cache is lost when PHP-FPM or Apache restarts:

bash
# Restart PHP-FPM
sudo systemctl restart php8.4-fpm
# All APCu cache is cleared

# Restart Apache
sudo systemctl restart apache2
# All APCu cache is cleared

For persistent cache, use SQLite or File driver.

Memory Limitations:

APCu is limited by configured shared memory size:

php
// If memory is full, oldest entries are evicted
$cache = cache('apcu');

// This might evict older entries
$cache->set('large:data', $largeArray, 3600);

When to Use APCu

Ideal For:

  • Single-server production deployments
  • Caching configuration and compiled data
  • Frequently accessed, rarely changing data
  • Applications requiring maximum performance
  • Reducing database load

Not Suitable For:

  • Distributed systems (multiple servers)
  • User-specific session data (use Redis)
  • Large datasets (limited by memory)
  • Data that must survive restarts

Troubleshooting

Extension Not Loaded:

bash
# Check if installed
php -m | grep apcu

# If not installed
pecl install apcu

# Enable in php.ini
echo "extension=apcu.so" >> /etc/php/8.4/cli/php.ini
echo "extension=apcu.so" >> /etc/php/8.4/fpm/php.ini

# Restart PHP-FPM
sudo systemctl restart php8.4-fpm

APCu Not Enabled:

bash
# Check configuration
php -i | grep apc.enabled

# Enable in php.ini
apc.enabled=1

# Restart PHP-FPM
sudo systemctl restart php8.4-fpm

Memory Exhausted:

ini
# Increase shared memory size in php.ini
apc.shm_size=128M

# Restart PHP-FPM
sudo systemctl restart php8.4-fpm

CLI Not Working:

ini
# Enable APCu for CLI in php.ini
apc.enable_cli=1

Performance Comparison

Driver    | Speed  | Persistent | Distributed | Setup
----------|--------|------------|-------------|-------
APCu      | Best   | No         | No          | Easy
Redis     | Best   | Optional   | Yes         | Medium
SQLite    | Good   | Yes        | No          | Easy
File      | Fair   | Yes        | No          | Easy

APCu provides the best performance for single-server deployments where cache doesn't need to survive restarts or be shared across servers.

7.6 Basic Cache Operations

ElliePHP's cache system provides a simple, consistent API for all cache operations across all drivers. The PSR-16 interface ensures predictable behavior regardless of which driver you're using.

The set() Method

Store a value in the cache with an optional TTL (Time To Live).

Signature:

php
public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool

Parameters:

  • key (string): Cache key identifier
  • value (mixed): Value to store (any serializable type)
  • ttl (null|int|DateInterval): Time to live in seconds, DateInterval, or null for permanent

Returns: true on success, false on failure

Basic Usage:

php
use function cache;

$cache = cache();

// Store string
$cache->set('app:name', 'ElliePHP', 3600);

// Store array
$cache->set('user:123', [
    'id' => 123,
    'name' => 'John Doe',
    'email' => 'john@example.com'
], 3600);

// Store object
$user = new User(123, 'John Doe');
$cache->set('user:object:123', $user, 3600);

// Store integer
$cache->set('counter:visits', 1000, 3600);

// Store boolean
$cache->set('feature:enabled', true, 3600);

TTL Examples:

php
// Cache for 1 hour (3600 seconds)
$cache->set('key', 'value', 3600);

// Cache for 5 minutes
$cache->set('key', 'value', 300);

// Cache for 24 hours
$cache->set('key', 'value', 86400);

// Cache permanently (no expiration)
$cache->set('key', 'value', null);
$cache->set('key', 'value', 0);

// Cache with DateInterval (1 hour)
$cache->set('key', 'value', new \DateInterval('PT1H'));

Complex Data Types:

php
// Nested arrays
$cache->set('config:database', [
    'host' => 'localhost',
    'port' => 3306,
    'credentials' => [
        'username' => 'root',
        'password' => 'secret'
    ]
], 3600);

// Associative arrays
$cache->set('users:active', [
    123 => 'John Doe',
    456 => 'Jane Smith',
    789 => 'Bob Johnson'
], 600);

The get() Method

Retrieve a value from the cache.

Signature:

php
public function get(string $key, mixed $default = null): mixed

Parameters:

  • key (string): Cache key identifier
  • default (mixed): Default value if key doesn't exist or is expired

Returns: Cached value or default value

Basic Usage:

php
// Get value
$name = $cache->get('app:name');

// Get with default value
$name = $cache->get('app:name', 'Default App');

// Get array
$user = $cache->get('user:123');
if ($user !== null) {
    echo $user['name'];
}

// Get with type checking
$counter = $cache->get('counter:visits', 0);
$counter++; // Safe because default is 0

Cache Miss Handling:

php
// Check if value exists
$value = $cache->get('key');

if ($value === null) {
    // Cache miss - fetch from source
    $value = fetchFromDatabase();
    
    // Store in cache
    $cache->set('key', $value, 3600);
}

return $value;

Default Values:

php
// String default
$name = $cache->get('user:name', 'Guest');

// Array default
$settings = $cache->get('user:settings', [
    'theme' => 'light',
    'language' => 'en'
]);

// Integer default
$count = $cache->get('page:views', 0);

// Boolean default
$enabled = $cache->get('feature:enabled', false);

The has() Method

Check if a key exists in the cache and is not expired.

Signature:

php
public function has(string $key): bool

Parameters:

  • key (string): Cache key identifier

Returns: true if key exists and is not expired, false otherwise

Usage:

php
// Check before getting
if ($cache->has('user:123')) {
    $user = $cache->get('user:123');
    echo "User found in cache";
} else {
    echo "User not in cache";
}

// Conditional caching
if (!$cache->has('expensive:data')) {
    $data = performExpensiveOperation();
    $cache->set('expensive:data', $data, 3600);
}

// Existence check
$keys = ['user:123', 'user:456', 'user:789'];
foreach ($keys as $key) {
    if ($cache->has($key)) {
        echo "{$key} is cached\n";
    }
}

Important Note:

has() returns false for expired keys, even if they haven't been physically deleted yet:

php
// Set with 1 second TTL
$cache->set('key', 'value', 1);

// Immediately check
var_dump($cache->has('key')); // true

// Wait 2 seconds
sleep(2);

// Check again
var_dump($cache->has('key')); // false (expired)

The delete() Method

Remove a specific key from the cache.

Signature:

php
public function delete(string $key): bool

Parameters:

  • key (string): Cache key identifier

Returns: true on success, false on failure

Usage:

php
// Delete single key
$cache->delete('user:123');

// Delete after update
function updateUser(int $id, array $data): void
{
    // Update database
    $database->update('users', $id, $data);
    
    // Invalidate cache
    $cache = cache();
    $cache->delete("user:{$id}");
}

// Conditional delete
if ($cache->has('old:data')) {
    $cache->delete('old:data');
}

// Delete multiple related keys
$userId = 123;
$cache->delete("user:{$userId}");
$cache->delete("user:{$userId}:profile");
$cache->delete("user:{$userId}:settings");

Return Value:

php
// Check if deletion succeeded
if ($cache->delete('key')) {
    echo "Key deleted successfully";
} else {
    echo "Key deletion failed or key didn't exist";
}

The clear() Method

Remove all entries from the cache.

Signature:

php
public function clear(): bool

Returns: true on success, false on failure

Usage:

php
// Clear all cache
$cache = cache();
$cache->clear();

// Clear on deployment
function clearCacheOnDeploy(): void
{
    $cache = cache();
    
    if ($cache->clear()) {
        echo "Cache cleared successfully";
    } else {
        echo "Failed to clear cache";
    }
}

// Clear specific driver
cache('file')->clear();
cache('redis')->clear();
cache('sqlite')->clear();
cache('apcu')->clear();

Use Cases:

php
// Clear cache after configuration change
function updateConfiguration(array $newConfig): void
{
    // Save configuration
    saveConfig($newConfig);
    
    // Clear all cached config
    cache()->clear();
}

// Clear cache in console command
public function handle(): int
{
    $this->info('Clearing cache...');
    
    cache()->clear();
    
    $this->success('Cache cleared successfully');
    
    return self::SUCCESS;
}

Complete Example: User Caching

Here's a complete example demonstrating all basic cache operations:

php
class UserService
{
    private Cache $cache;
    
    public function __construct()
    {
        $this->cache = cache();
    }
    
    public function getUser(int $id): ?array
    {
        $cacheKey = "user:{$id}";
        
        // Check if user is cached
        if ($this->cache->has($cacheKey)) {
            return $this->cache->get($cacheKey);
        }
        
        // Fetch from database
        $user = $this->fetchUserFromDatabase($id);
        
        if ($user !== null) {
            // Cache for 1 hour
            $this->cache->set($cacheKey, $user, 3600);
        }
        
        return $user;
    }
    
    public function updateUser(int $id, array $data): bool
    {
        // Update database
        $success = $this->updateUserInDatabase($id, $data);
        
        if ($success) {
            // Invalidate cache
            $this->cache->delete("user:{$id}");
        }
        
        return $success;
    }
    
    public function deleteUser(int $id): bool
    {
        // Delete from database
        $success = $this->deleteUserFromDatabase($id);
        
        if ($success) {
            // Remove from cache
            $this->cache->delete("user:{$id}");
        }
        
        return $success;
    }
    
    public function clearAllUsers(): void
    {
        // Clear entire cache
        // Note: This clears ALL cache, not just users
        $this->cache->clear();
    }
    
    private function fetchUserFromDatabase(int $id): ?array
    {
        // Database query implementation
        return [
            'id' => $id,
            'name' => 'John Doe',
            'email' => 'john@example.com'
        ];
    }
    
    private function updateUserInDatabase(int $id, array $data): bool
    {
        // Database update implementation
        return true;
    }
    
    private function deleteUserFromDatabase(int $id): bool
    {
        // Database delete implementation
        return true;
    }
}

Cache-Aside Pattern

The most common caching pattern:

php
function getCachedData(string $key, callable $loader, int $ttl = 3600): mixed
{
    $cache = cache();
    
    // Try to get from cache
    $data = $cache->get($key);
    
    if ($data === null) {
        // Cache miss - load data
        $data = $loader();
        
        // Store in cache
        $cache->set($key, $data, $ttl);
    }
    
    return $data;
}

// Usage
$user = getCachedData(
    'user:123',
    fn() => $database->find('users', 123),
    3600
);

Error Handling

All cache operations return boolean values or throw exceptions:

php
use ElliePHP\Components\Cache\Exceptions\CacheException;
use ElliePHP\Components\Cache\Exceptions\InvalidArgumentException;

try {
    $cache = cache();
    
    // Invalid key (contains {})
    $cache->set('invalid{key}', 'value');
    
} catch (InvalidArgumentException $e) {
    // Handle invalid key
    echo "Invalid cache key: " . $e->getMessage();
    
} catch (CacheException $e) {
    // Handle cache operation failure
    echo "Cache operation failed: " . $e->getMessage();
}

Best Practices

1. Use Descriptive Keys:

php
// Good: Clear, hierarchical keys
$cache->set('user:123:profile', $profile);
$cache->set('post:456:comments', $comments);
$cache->set('api:github:user:octocat', $data);

// Bad: Unclear keys
$cache->set('u123', $profile);
$cache->set('data', $comments);

2. Set Appropriate TTLs:

php
// Frequently changing data (5 minutes)
$cache->set('stock:price:AAPL', $price, 300);

// Moderately changing data (1 hour)
$cache->set('user:123:profile', $profile, 3600);

// Rarely changing data (24 hours)
$cache->set('config:app', $config, 86400);

// Static data (permanent)
$cache->set('app:version', '1.0.0', null);

3. Always Provide Defaults:

php
// Good: Safe default value
$count = $cache->get('page:views', 0);
$count++; // Safe

// Bad: No default, potential null error
$count = $cache->get('page:views');
$count++; // Error if null

4. Invalidate on Updates:

php
// Always invalidate cache after updates
function updatePost(int $id, array $data): void
{
    $database->update('posts', $id, $data);
    
    // Invalidate related caches
    cache()->delete("post:{$id}");
    cache()->delete("post:{$id}:comments");
    cache()->delete("user:{$data['author_id']}:posts");
}

5. Handle Cache Failures Gracefully:

php
function getData(int $id): array
{
    try {
        $cache = cache();
        
        $data = $cache->get("data:{$id}");
        
        if ($data === null) {
            $data = fetchFromDatabase($id);
            $cache->set("data:{$id}", $data, 3600);
        }
        
        return $data;
        
    } catch (CacheException $e) {
        // Cache failed - fetch directly
        report()->warning('Cache operation failed', [
            'error' => $e->getMessage()
        ]);
        
        return fetchFromDatabase($id);
    }
}

7.7 Batch Cache Operations

Batch operations allow you to work with multiple cache entries in a single operation, significantly improving performance when dealing with multiple keys. Instead of making individual calls for each key, batch operations reduce overhead and network round-trips.

The getMultiple() Method

Retrieve multiple values from the cache in a single operation.

Signature:

php
public function getMultiple(iterable $keys, mixed $default = null): iterable

Parameters:

  • keys (iterable): Array or iterable of cache keys
  • default (mixed): Default value for missing keys

Returns: Iterable of key-value pairs

Basic Usage:

php
use function cache;

$cache = cache();

// Get multiple users
$keys = ['user:123', 'user:456', 'user:789'];
$users = $cache->getMultiple($keys);

// Result: ['user:123' => [...], 'user:456' => [...], 'user:789' => null]
foreach ($users as $key => $user) {
    if ($user !== null) {
        echo "Found user: {$user['name']}\n";
    }
}

With Default Values:

php
// Get multiple settings with defaults
$keys = ['setting:theme', 'setting:language', 'setting:timezone'];
$settings = $cache->getMultiple($keys, 'default');

// Result: ['setting:theme' => 'dark', 'setting:language' => 'default', ...]

Performance Benefits:

php
// Slow: Individual get() calls
$user1 = $cache->get('user:1');
$user2 = $cache->get('user:2');
$user3 = $cache->get('user:3');
// 3 separate operations

// Fast: Single getMultiple() call
$users = $cache->getMultiple(['user:1', 'user:2', 'user:3']);
// 1 batch operation

Real-World Example:

php
function getUsersWithCache(array $userIds): array
{
    $cache = cache();
    
    // Build cache keys
    $keys = array_map(fn($id) => "user:{$id}", $userIds);
    
    // Get all users in one operation
    $cachedUsers = $cache->getMultiple($keys);
    
    $users = [];
    $missingIds = [];
    
    foreach ($userIds as $id) {
        $key = "user:{$id}";
        
        if (isset($cachedUsers[$key]) && $cachedUsers[$key] !== null) {
            // Cache hit
            $users[$id] = $cachedUsers[$key];
        } else {
            // Cache miss
            $missingIds[] = $id;
        }
    }
    
    // Fetch missing users from database
    if (!empty($missingIds)) {
        $fetchedUsers = fetchUsersFromDatabase($missingIds);
        
        // Cache the fetched users
        $toCache = [];
        foreach ($fetchedUsers as $user) {
            $toCache["user:{$user['id']}"] = $user;
            $users[$user['id']] = $user;
        }
        
        $cache->setMultiple($toCache, 3600);
    }
    
    return $users;
}

The setMultiple() Method

Store multiple values in the cache in a single operation.

Signature:

php
public function setMultiple(iterable $values, null|int|\DateInterval $ttl = null): bool

Parameters:

  • values (iterable): Array or iterable of key-value pairs
  • ttl (null|int|DateInterval): Time to live for all entries

Returns: true on success, false on failure

Basic Usage:

php
$cache = cache();

// Store multiple users
$users = [
    'user:123' => ['id' => 123, 'name' => 'John Doe'],
    'user:456' => ['id' => 456, 'name' => 'Jane Smith'],
    'user:789' => ['id' => 789, 'name' => 'Bob Johnson']
];

$cache->setMultiple($users, 3600); // Cache all for 1 hour

Caching Query Results:

php
function cacheQueryResults(array $results): void
{
    $cache = cache();
    $toCache = [];
    
    foreach ($results as $row) {
        $key = "product:{$row['id']}";
        $toCache[$key] = $row;
    }
    
    // Cache all products in one operation
    $cache->setMultiple($toCache, 3600);
}

Configuration Caching:

php
function cacheAllSettings(array $settings): void
{
    $cache = cache();
    $toCache = [];
    
    foreach ($settings as $key => $value) {
        $toCache["setting:{$key}"] = $value;
    }
    
    // Cache all settings permanently
    $cache->setMultiple($toCache, null);
}

Performance Comparison:

php
// Slow: Individual set() calls (100 operations)
for ($i = 0; $i < 100; $i++) {
    $cache->set("key:{$i}", $data[$i], 3600);
}
// Time: ~500ms (Redis), ~2000ms (File)

// Fast: Single setMultiple() call (1 operation)
$toCache = [];
for ($i = 0; $i < 100; $i++) {
    $toCache["key:{$i}"] = $data[$i];
}
$cache->setMultiple($toCache, 3600);
// Time: ~50ms (Redis), ~500ms (File)

The deleteMultiple() Method

Delete multiple keys from the cache in a single operation.

Signature:

php
public function deleteMultiple(iterable $keys): bool

Parameters:

  • keys (iterable): Array or iterable of cache keys to delete

Returns: true on success, false on failure

Basic Usage:

php
$cache = cache();

// Delete multiple users
$keys = ['user:123', 'user:456', 'user:789'];
$cache->deleteMultiple($keys);

Invalidating Related Cache:

php
function invalidateUserCache(int $userId): void
{
    $cache = cache();
    
    // Delete all user-related cache entries
    $keys = [
        "user:{$userId}",
        "user:{$userId}:profile",
        "user:{$userId}:settings",
        "user:{$userId}:posts",
        "user:{$userId}:comments"
    ];
    
    $cache->deleteMultiple($keys);
}

Bulk Cache Invalidation:

php
function clearProductCache(array $productIds): void
{
    $cache = cache();
    
    // Build keys for all products
    $keys = array_map(fn($id) => "product:{$id}", $productIds);
    
    // Delete all in one operation
    $cache->deleteMultiple($keys);
}

Performance Benefits

Batch operations provide significant performance improvements, especially with Redis:

Network Round-Trips:

php
// Individual operations: 100 network round-trips
for ($i = 0; $i < 100; $i++) {
    $cache->get("key:{$i}");
}
// Time: ~100ms (1ms per round-trip)

// Batch operation: 1 network round-trip
$keys = array_map(fn($i) => "key:{$i}", range(0, 99));
$cache->getMultiple($keys);
// Time: ~10ms (single round-trip)

Driver-Specific Performance:

Operation          | Individual (100 keys) | Batch (100 keys) | Improvement
-------------------|----------------------|------------------|-------------
Redis GET          | ~100ms               | ~10ms            | 10x faster
Redis SET          | ~100ms               | ~15ms            | 6x faster
File GET           | ~2000ms              | ~500ms           | 4x faster
File SET           | ~3000ms              | ~800ms           | 3.7x faster
SQLite GET         | ~1000ms              | ~300ms           | 3.3x faster
SQLite SET         | ~1500ms              | ~500ms           | 3x faster
APCu GET           | ~50ms                | ~10ms            | 5x faster
APCu SET           | ~60ms                | ~15ms            | 4x faster

Complete Example: Product Catalog

Here's a complete example using batch operations for a product catalog:

php
class ProductCatalog
{
    private Cache $cache;
    
    public function __construct()
    {
        $this->cache = cache();
    }
    
    public function getProducts(array $productIds): array
    {
        // Build cache keys
        $keys = array_map(fn($id) => "product:{$id}", $productIds);
        
        // Get all products from cache
        $cached = $this->cache->getMultiple($keys);
        
        $products = [];
        $missingIds = [];
        
        // Separate cached and missing products
        foreach ($productIds as $id) {
            $key = "product:{$id}";
            
            if (isset($cached[$key]) && $cached[$key] !== null) {
                $products[$id] = $cached[$key];
            } else {
                $missingIds[] = $id;
            }
        }
        
        // Fetch missing products from database
        if (!empty($missingIds)) {
            $fetched = $this->fetchProductsFromDatabase($missingIds);
            
            // Prepare for batch caching
            $toCache = [];
            foreach ($fetched as $product) {
                $toCache["product:{$product['id']}"] = $product;
                $products[$product['id']] = $product;
            }
            
            // Cache all fetched products
            $this->cache->setMultiple($toCache, 3600);
        }
        
        return $products;
    }
    
    public function updateProducts(array $products): void
    {
        // Update database
        $this->updateProductsInDatabase($products);
        
        // Invalidate cache for all updated products
        $keys = array_map(fn($p) => "product:{$p['id']}", $products);
        $this->cache->deleteMultiple($keys);
    }
    
    public function cacheAllProducts(): void
    {
        // Fetch all products
        $products = $this->fetchAllProductsFromDatabase();
        
        // Prepare batch cache
        $toCache = [];
        foreach ($products as $product) {
            $toCache["product:{$product['id']}"] = $product;
        }
        
        // Cache all products at once
        $this->cache->setMultiple($toCache, 3600);
        
        echo "Cached " . count($products) . " products\n";
    }
    
    private function fetchProductsFromDatabase(array $ids): array
    {
        // Database query implementation
        return [];
    }
    
    private function fetchAllProductsFromDatabase(): array
    {
        // Database query implementation
        return [];
    }
    
    private function updateProductsInDatabase(array $products): void
    {
        // Database update implementation
    }
}

Best Practices

1. Use Batch Operations for Multiple Keys:

php
// Good: Batch operation
$users = $cache->getMultiple(['user:1', 'user:2', 'user:3']);

// Bad: Individual operations
$user1 = $cache->get('user:1');
$user2 = $cache->get('user:2');
$user3 = $cache->get('user:3');

2. Limit Batch Size:

php
// Good: Process in chunks
$allKeys = range(1, 10000);
$chunks = array_chunk($allKeys, 100);

foreach ($chunks as $chunk) {
    $keys = array_map(fn($id) => "key:{$id}", $chunk);
    $results = $cache->getMultiple($keys);
    processResults($results);
}

// Bad: Single huge batch
$keys = array_map(fn($id) => "key:{$id}", range(1, 10000));
$results = $cache->getMultiple($keys); // May timeout or use too much memory

3. Handle Partial Failures:

php
try {
    $cache->setMultiple($data, 3600);
} catch (CacheException $e) {
    // Log error but continue
    report()->error('Batch cache operation failed', [
        'error' => $e->getMessage(),
        'keys' => array_keys($data)
    ]);
}

4. Use Consistent Key Patterns:

php
// Good: Consistent pattern
$keys = [
    'user:123:profile',
    'user:123:settings',
    'user:123:preferences'
];

// Bad: Inconsistent pattern
$keys = [
    'user_123_profile',
    'settings:user:123',
    'user-preferences-123'
];

5. Combine with Cache-Aside Pattern:

php
function getCachedItems(array $ids, callable $loader): array
{
    $cache = cache();
    
    // Build keys
    $keys = array_map(fn($id) => "item:{$id}", $ids);
    
    // Get from cache
    $cached = $cache->getMultiple($keys);
    
    $items = [];
    $missingIds = [];
    
    // Identify cache misses
    foreach ($ids as $id) {
        $key = "item:{$id}";
        if (isset($cached[$key]) && $cached[$key] !== null) {
            $items[$id] = $cached[$key];
        } else {
            $missingIds[] = $id;
        }
    }
    
    // Load missing items
    if (!empty($missingIds)) {
        $loaded = $loader($missingIds);
        
        // Cache loaded items
        $toCache = [];
        foreach ($loaded as $item) {
            $toCache["item:{$item['id']}"] = $item;
            $items[$item['id']] = $item;
        }
        
        $cache->setMultiple($toCache, 3600);
    }
    
    return $items;
}

Error Handling

php
use ElliePHP\Components\Cache\Exceptions\CacheException;
use ElliePHP\Components\Cache\Exceptions\InvalidArgumentException;

try {
    $cache = cache();
    
    // Batch operations
    $cache->setMultiple($data, 3600);
    $results = $cache->getMultiple($keys);
    $cache->deleteMultiple($keys);
    
} catch (InvalidArgumentException $e) {
    // Invalid keys in batch
    echo "Invalid cache keys: " . $e->getMessage();
    
} catch (CacheException $e) {
    // Cache operation failed
    echo "Batch operation failed: " . $e->getMessage();
}

When to Use Batch Operations

Use Batch Operations When:

  • Working with multiple related cache entries
  • Fetching data for multiple entities (users, products, etc.)
  • Invalidating related cache entries
  • Pre-warming cache with multiple entries
  • Performance is critical

Use Individual Operations When:

  • Working with a single cache entry
  • Keys are processed one at a time
  • Immediate feedback needed for each operation
  • Batch size would be very small (1-2 keys)

Performance Tuning

Optimal Batch Sizes:

php
// Redis: 100-1000 keys per batch
$chunks = array_chunk($allKeys, 500);

// File/SQLite: 50-100 keys per batch
$chunks = array_chunk($allKeys, 50);

// APCu: 100-500 keys per batch
$chunks = array_chunk($allKeys, 200);

Parallel Processing:

php
// Process multiple batches in parallel (if using Redis)
$batches = array_chunk($allKeys, 100);

foreach ($batches as $batch) {
    // Each batch is independent
    $results = $cache->getMultiple($batch);
    processResults($results);
}

7.8 TTL and Expiration

TTL (Time To Live) controls how long cache entries remain valid before expiring. ElliePHP supports multiple TTL formats and provides flexible expiration strategies to suit different caching needs.

TTL in Seconds

The most common way to specify TTL is in seconds as an integer.

Basic Usage:

php
use function cache;

$cache = cache();

// Cache for 1 minute (60 seconds)
$cache->set('key', 'value', 60);

// Cache for 5 minutes (300 seconds)
$cache->set('key', 'value', 300);

// Cache for 1 hour (3600 seconds)
$cache->set('key', 'value', 3600);

// Cache for 24 hours (86400 seconds)
$cache->set('key', 'value', 86400);

// Cache for 7 days (604800 seconds)
$cache->set('key', 'value', 604800);

// Cache for 30 days (2592000 seconds)
$cache->set('key', 'value', 2592000);

Common TTL Values:

php
// Short-lived data
const TTL_1_MINUTE = 60;
const TTL_5_MINUTES = 300;
const TTL_15_MINUTES = 900;

// Medium-lived data
const TTL_1_HOUR = 3600;
const TTL_6_HOURS = 21600;
const TTL_12_HOURS = 43200;

// Long-lived data
const TTL_1_DAY = 86400;
const TTL_1_WEEK = 604800;
const TTL_1_MONTH = 2592000;

// Usage
$cache->set('session:token', $token, TTL_1_HOUR);
$cache->set('user:profile', $profile, TTL_1_DAY);
$cache->set('config:settings', $settings, TTL_1_WEEK);

Calculating TTL:

php
// Cache until end of day
$secondsUntilMidnight = strtotime('tomorrow') - time();
$cache->set('daily:stats', $stats, $secondsUntilMidnight);

// Cache until specific time
$expiryTime = strtotime('2024-12-31 23:59:59');
$ttl = $expiryTime - time();
$cache->set('year:end:data', $data, $ttl);

// Cache for business hours (8 hours)
$cache->set('business:data', $data, 8 * 3600);

DateInterval Support

For more readable and flexible TTL specifications, use PHP's DateInterval class.

Basic DateInterval Usage:

php
use DateInterval;

$cache = cache();

// Cache for 1 hour
$cache->set('key', 'value', new DateInterval('PT1H'));

// Cache for 30 minutes
$cache->set('key', 'value', new DateInterval('PT30M'));

// Cache for 1 day
$cache->set('key', 'value', new DateInterval('P1D'));

// Cache for 1 week
$cache->set('key', 'value', new DateInterval('P7D'));

// Cache for 1 month
$cache->set('key', 'value', new DateInterval('P1M'));

// Cache for 1 year
$cache->set('key', 'value', new DateInterval('P1Y'));

DateInterval Format:

The format is: P[n]Y[n]M[n]DT[n]H[n]M[n]S

  • P - Period designator (required)
  • Y - Years
  • M - Months (before T) or Minutes (after T)
  • D - Days
  • T - Time designator (separates date and time)
  • H - Hours
  • M - Minutes
  • S - Seconds

Examples:

php
// 2 hours 30 minutes
$cache->set('key', 'value', new DateInterval('PT2H30M'));

// 1 day 12 hours
$cache->set('key', 'value', new DateInterval('P1DT12H'));

// 3 days 6 hours 30 minutes
$cache->set('key', 'value', new DateInterval('P3DT6H30M'));

// 1 week (7 days)
$cache->set('key', 'value', new DateInterval('P7D'));

// 90 days
$cache->set('key', 'value', new DateInterval('P90D'));

Helper Function for DateInterval:

php
function createTTL(int $hours = 0, int $minutes = 0, int $seconds = 0): DateInterval
{
    $spec = 'PT';
    
    if ($hours > 0) {
        $spec .= "{$hours}H";
    }
    
    if ($minutes > 0) {
        $spec .= "{$minutes}M";
    }
    
    if ($seconds > 0) {
        $spec .= "{$seconds}S";
    }
    
    return new DateInterval($spec);
}

// Usage
$cache->set('key', 'value', createTTL(hours: 2, minutes: 30));
$cache->set('key', 'value', createTTL(minutes: 15));
$cache->set('key', 'value', createTTL(hours: 24));

Permanent Cache (No Expiration)

Cache entries can be stored permanently without expiration by using null or 0 as the TTL.

Permanent Cache:

php
$cache = cache();

// Cache permanently (never expires)
$cache->set('app:version', '1.0.0', null);
$cache->set('config:constants', $constants, 0);

// Both are equivalent
$cache->set('key', 'value', null);
$cache->set('key', 'value', 0);

Use Cases for Permanent Cache:

php
// Application version
$cache->set('app:version', '1.0.0', null);

// Static configuration
$cache->set('config:app', $appConfig, null);

// Compiled routes
$cache->set('routes:compiled', $routes, null);

// Compiled templates
$cache->set('template:compiled:home', $compiled, null);

// Feature flags
$cache->set('feature:new_ui', true, null);

Important Notes:

  • Permanent cache entries remain until explicitly deleted or cache is cleared
  • Use permanent cache for data that rarely changes
  • Remember to invalidate permanent cache when data changes
  • Permanent cache survives application restarts (except APCu)

Invalidating Permanent Cache:

php
// Update configuration
function updateConfig(array $newConfig): void
{
    // Save to file
    saveConfigToFile($newConfig);
    
    // Invalidate permanent cache
    cache()->delete('config:app');
    
    // Next request will reload from file
}

// Update application version
function deployNewVersion(string $version): void
{
    // Update version file
    file_put_contents('VERSION', $version);
    
    // Invalidate version cache
    cache()->delete('app:version');
}

TTL Best Practices

1. Match TTL to Data Volatility:

php
// Highly volatile data (changes frequently)
$cache->set('stock:price:AAPL', $price, 60); // 1 minute

// Moderately volatile data
$cache->set('user:session', $session, 1800); // 30 minutes

// Stable data (changes rarely)
$cache->set('user:profile', $profile, 86400); // 24 hours

// Static data (never changes)
$cache->set('app:config', $config, null); // Permanent

2. Consider Cache Stampede:

php
// Bad: All entries expire at same time
for ($i = 0; $i < 1000; $i++) {
    $cache->set("product:{$i}", $data, 3600);
}
// All expire after 1 hour, causing stampede

// Good: Add random jitter to TTL
for ($i = 0; $i < 1000; $i++) {
    $ttl = 3600 + rand(0, 300); // 1 hour ± 5 minutes
    $cache->set("product:{$i}", $data, $ttl);
}
// Entries expire gradually

3. Use Shorter TTL for Critical Data:

php
// Critical data: Short TTL for freshness
$cache->set('payment:status', $status, 300); // 5 minutes

// Non-critical data: Longer TTL for performance
$cache->set('blog:posts', $posts, 3600); // 1 hour

4. Implement Soft Expiration:

php
function getCachedWithSoftExpiry(string $key, callable $loader, int $ttl): mixed
{
    $cache = cache();
    $softKey = "{$key}:soft";
    
    // Try to get fresh data
    $data = $cache->get($key);
    
    if ($data !== null) {
        return $data;
    }
    
    // Try to get stale data
    $staleData = $cache->get($softKey);
    
    if ($staleData !== null) {
        // Return stale data immediately
        // Refresh in background
        refreshInBackground($key, $loader, $ttl);
        return $staleData;
    }
    
    // No cache - load fresh
    $data = $loader();
    
    // Store with normal TTL
    $cache->set($key, $data, $ttl);
    
    // Store with extended TTL for soft expiry
    $cache->set($softKey, $data, $ttl * 2);
    
    return $data;
}

5. Document TTL Choices:

php
class CacheTTL
{
    // API rate limiting
    const RATE_LIMIT = 60; // 1 minute
    
    // User sessions
    const SESSION = 1800; // 30 minutes
    
    // User profiles
    const USER_PROFILE = 3600; // 1 hour
    
    // Product catalog
    const PRODUCT = 7200; // 2 hours
    
    // Static content
    const STATIC_CONTENT = 86400; // 24 hours
    
    // Configuration
    const CONFIG = null; // Permanent
}

// Usage
$cache->set('user:123', $user, CacheTTL::USER_PROFILE);
$cache->set('product:456', $product, CacheTTL::PRODUCT);

Expiration Behavior

How Expiration Works:

php
// Set with 10 second TTL
$cache->set('key', 'value', 10);

// Immediately after
var_dump($cache->has('key')); // true
var_dump($cache->get('key')); // 'value'

// After 5 seconds
sleep(5);
var_dump($cache->has('key')); // true
var_dump($cache->get('key')); // 'value'

// After 11 seconds (expired)
sleep(6);
var_dump($cache->has('key')); // false
var_dump($cache->get('key')); // null

Driver-Specific Expiration:

php
// Redis: Automatic expiration
// Expired entries are automatically removed by Redis

// File/SQLite: Lazy expiration
// Expired entries remain until accessed or manually cleared
$driver->clearExpired(); // Manual cleanup

// APCu: Automatic expiration
// Expired entries are automatically removed by APCu

TTL Examples by Use Case

Session Management:

php
// User session (30 minutes)
$cache->set("session:{$sessionId}", $sessionData, 1800);

// Remember me token (30 days)
$cache->set("remember:{$token}", $userId, 2592000);

// CSRF token (1 hour)
$cache->set("csrf:{$token}", true, 3600);

API Caching:

php
// API response (5 minutes)
$cache->set("api:response:{$endpoint}", $response, 300);

// API rate limit (1 minute)
$cache->set("api:ratelimit:{$userId}", $count, 60);

// API token (1 hour)
$cache->set("api:token:{$userId}", $token, 3600);

Database Query Caching:

php
// Frequently changing data (5 minutes)
$cache->set('query:recent_posts', $posts, 300);

// Moderately changing data (1 hour)
$cache->set('query:user_stats', $stats, 3600);

// Rarely changing data (24 hours)
$cache->set('query:categories', $categories, 86400);

Computed Values:

php
// Real-time calculations (1 minute)
$cache->set('calc:stock_price', $price, 60);

// Periodic calculations (15 minutes)
$cache->set('calc:dashboard_stats', $stats, 900);

// Daily calculations (24 hours)
$cache->set('calc:daily_report', $report, 86400);

Monitoring Expiration

Track Cache Age:

php
function setCacheWithTimestamp(string $key, mixed $value, int $ttl): void
{
    $cache = cache();
    
    $data = [
        'value' => $value,
        'cached_at' => time(),
        'expires_at' => time() + $ttl
    ];
    
    $cache->set($key, $data, $ttl);
}

function getCacheAge(string $key): ?int
{
    $cache = cache();
    $data = $cache->get($key);
    
    if ($data === null) {
        return null;
    }
    
    return time() - $data['cached_at'];
}

// Usage
$age = getCacheAge('user:123');
echo "Cache age: {$age} seconds\n";

Log Cache Expiration:

php
function getCachedWithLogging(string $key, callable $loader, int $ttl): mixed
{
    $cache = cache();
    $data = $cache->get($key);
    
    if ($data === null) {
        report()->info('Cache miss', ['key' => $key]);
        
        $data = $loader();
        $cache->set($key, $data, $ttl);
        
        report()->info('Cache set', [
            'key' => $key,
            'ttl' => $ttl,
            'expires_at' => date('Y-m-d H:i:s', time() + $ttl)
        ]);
    } else {
        report()->debug('Cache hit', ['key' => $key]);
    }
    
    return $data;
}

7.9 Cache Helper Function

ElliePHP provides a convenient cache() global helper function that simplifies cache access and driver selection. This function is the recommended way to interact with the cache system.

Function Signature

php
function cache(?string $cacheDriver = null): Cache

Parameters:

  • cacheDriver (string|null): Cache driver name ('file', 'redis', 'sqlite', 'apcu'), or null to use CACHE_DRIVER from environment

Returns: Cache instance configured with the specified driver

Basic Usage

Using Default Driver:

php
use function cache;

// Uses CACHE_DRIVER from .env
$cache = cache();

// Perform cache operations
$cache->set('key', 'value', 3600);
$value = $cache->get('key');

Specifying Driver:

php
// Use file cache
$cache = cache('file');

// Use Redis cache
$cache = cache('redis');

// Use SQLite cache
$cache = cache('sqlite');

// Use APCu cache
$cache = cache('apcu');

Environment Variable Configuration

The cache() function reads the CACHE_DRIVER environment variable when no driver is specified.

Configure in .env:

env
# Cache Configuration
CACHE_DRIVER=file

# Redis Configuration (when using redis driver)
REDIS_HOST='127.0.0.1'
REDIS_PORT=6379
REDIS_PASSWORD=null
REDIS_DATABASE=0
REDIS_TIMEOUT=5

Driver Selection:

php
// .env: CACHE_DRIVER=file
$cache = cache(); // Uses file driver

// .env: CACHE_DRIVER=redis
$cache = cache(); // Uses redis driver

// .env: CACHE_DRIVER=sqlite
$cache = cache(); // Uses sqlite driver

// .env: CACHE_DRIVER=apcu
$cache = cache(); // Uses apcu driver

Automatic Driver Configuration

The cache() function automatically configures each driver with appropriate settings:

File Driver Configuration:

php
// When using cache('file') or CACHE_DRIVER=file
$cache = cache('file');

// Automatically configured as:
$driver = CacheFactory::createFileDriver([
    'path' => storage_cache_path() // storage/Cache/
]);

Redis Driver Configuration:

php
// When using cache('redis') or CACHE_DRIVER=redis
$cache = cache('redis');

// Automatically configured from environment:
$driver = CacheFactory::createRedisDriver([
    'host' => env('REDIS_HOST', '127.0.0.1'),
    'port' => env('REDIS_PORT', 6379),
    'password' => env('REDIS_PASSWORD'),
    'database' => env('REDIS_DATABASE', 0),
    'timeout' => env('REDIS_TIMEOUT', 5),
]);

SQLite Driver Configuration:

php
// When using cache('sqlite') or CACHE_DRIVER=sqlite
$cache = cache('sqlite');

// Automatically configured as:
$driver = CacheFactory::createSQLiteDriver([
    'path' => storage_cache_path('cache.db'),
    'create_directory' => true,
    'directory_permissions' => 0755,
]);

APCu Driver Configuration:

php
// When using cache('apcu') or CACHE_DRIVER=apcu
$cache = cache('apcu');

// Automatically configured as:
$driver = CacheFactory::createApcuDriver();

Automatic Garbage Collection

The cache() function automatically calls clearExpired() for drivers that support it (File and SQLite):

php
// File driver: Automatically clears expired entries
$cache = cache('file');
// Equivalent to:
// $driver->clearExpired();
// $cache = new Cache($driver);

// SQLite driver: Automatically clears expired entries
$cache = cache('sqlite');
// Equivalent to:
// $driver->clearExpired();
// $cache = new Cache($driver);

// Redis and APCu: No manual cleanup needed
$cache = cache('redis'); // Automatic expiration
$cache = cache('apcu');  // Automatic expiration

Usage Examples

Simple Caching:

php
use function cache;

// Store data
cache()->set('user:123', $userData, 3600);

// Retrieve data
$user = cache()->get('user:123');

// Check existence
if (cache()->has('user:123')) {
    echo "User is cached";
}

// Delete data
cache()->delete('user:123');

Driver-Specific Operations:

php
// Use file cache for development
if (env('APP_ENV') === 'development') {
    $cache = cache('file');
} else {
    // Use Redis for production
    $cache = cache('redis');
}

$cache->set('data', $value, 3600);

Multiple Drivers:

php
// Cache in multiple drivers for redundancy
$fileCache = cache('file');
$redisCache = cache('redis');

// Store in both
$fileCache->set('key', 'value', 3600);
$redisCache->set('key', 'value', 3600);

// Try Redis first, fallback to file
$value = $redisCache->get('key') ?? $fileCache->get('key');

Inline Usage:

php
// Direct inline usage
cache()->set('config:app', $config, null);

$settings = cache()->get('config:app', []);

if (cache()->has('feature:enabled')) {
    enableFeature();
}

cache()->delete('old:data');

Environment-Based Driver Selection

Development vs Production:

env
# .env.development
CACHE_DRIVER=file

# .env.production
CACHE_DRIVER=redis
REDIS_HOST='redis.production.com'
REDIS_PORT=6379
REDIS_PASSWORD='secure-password'
php
// Same code works in both environments
$cache = cache();
$cache->set('data', $value, 3600);

Testing Environment:

env
# .env.testing
CACHE_DRIVER=file
php
// Tests use file cache for isolation
public function testCaching(): void
{
    $cache = cache();
    $cache->set('test:key', 'test:value', 60);
    
    $this->assertEquals('test:value', $cache->get('test:key'));
}

Advanced Usage Patterns

Dependency Injection:

php
class UserService
{
    private Cache $cache;
    
    public function __construct()
    {
        $this->cache = cache();
    }
    
    public function getUser(int $id): ?array
    {
        $cacheKey = "user:{$id}";
        
        $user = $this->cache->get($cacheKey);
        
        if ($user === null) {
            $user = $this->fetchFromDatabase($id);
            $this->cache->set($cacheKey, $user, 3600);
        }
        
        return $user;
    }
}

Singleton Pattern:

php
class CacheManager
{
    private static ?Cache $instance = null;
    
    public static function getInstance(): Cache
    {
        if (self::$instance === null) {
            self::$instance = cache();
        }
        
        return self::$instance;
    }
}

// Usage
$cache = CacheManager::getInstance();

Factory Pattern:

php
class CacheFactory
{
    public static function create(string $driver = null): Cache
    {
        return cache($driver);
    }
    
    public static function createForEnvironment(): Cache
    {
        $driver = match (env('APP_ENV')) {
            'production' => 'redis',
            'staging' => 'redis',
            'testing' => 'file',
            default => 'file'
        };
        
        return cache($driver);
    }
}

Fallback Strategy:

php
function getCacheWithFallback(): Cache
{
    // Try Redis first
    try {
        $cache = cache('redis');
        $cache->set('health_check', true, 10);
        
        if ($cache->get('health_check') === true) {
            return $cache;
        }
    } catch (Exception $e) {
        report()->warning('Redis unavailable, using file cache', [
            'error' => $e->getMessage()
        ]);
    }
    
    // Fallback to file cache
    return cache('file');
}

Helper Function vs Direct Instantiation

Using Helper Function (Recommended):

php
// Simple and clean
$cache = cache();
$cache->set('key', 'value', 3600);

Direct Instantiation:

php
// More verbose
use ElliePHP\Components\Cache\CacheFactory;
use ElliePHP\Components\Cache\Cache;

$driver = CacheFactory::createFileDriver([
    'path' => storage_cache_path()
]);
$cache = new Cache($driver);
$cache->set('key', 'value', 3600);

The helper function is recommended because it:

  • Automatically configures drivers from environment
  • Handles garbage collection
  • Provides cleaner, more readable code
  • Reduces boilerplate

Best Practices

1. Use Environment Configuration:

php
// Good: Uses environment configuration
$cache = cache();

// Avoid: Hardcoded driver
$cache = cache('redis'); // Only when specifically needed

2. Reuse Cache Instances:

php
// Good: Reuse instance
$cache = cache();
$cache->set('key1', 'value1', 3600);
$cache->set('key2', 'value2', 3600);

// Avoid: Creating multiple instances
cache()->set('key1', 'value1', 3600);
cache()->set('key2', 'value2', 3600);

3. Handle Driver Unavailability:

php
try {
    $cache = cache();
    $cache->set('key', 'value', 3600);
} catch (CacheException $e) {
    // Log error and continue without cache
    report()->error('Cache operation failed', [
        'error' => $e->getMessage()
    ]);
}

4. Document Driver Requirements:

php
/**
 * Get user from cache or database
 * 
 * Requires: CACHE_DRIVER environment variable
 * Supports: file, redis, sqlite, apcu
 */
function getCachedUser(int $id): ?array
{
    $cache = cache();
    // ...
}

5. Test with Different Drivers:

php
// Test with file cache
public function testCachingWithFile(): void
{
    putenv('CACHE_DRIVER=file');
    $cache = cache();
    // Test implementation
}

// Test with Redis (if available)
public function testCachingWithRedis(): void
{
    if (!extension_loaded('redis')) {
        $this->markTestSkipped('Redis extension not available');
    }
    
    putenv('CACHE_DRIVER=redis');
    $cache = cache();
    // Test implementation
}

Troubleshooting

Driver Not Found:

php
// Check if driver is supported
$supportedDrivers = ['file', 'redis', 'sqlite', 'apcu'];
$driver = env('CACHE_DRIVER', 'file');

if (!in_array($driver, $supportedDrivers)) {
    throw new Exception("Unsupported cache driver: {$driver}");
}

$cache = cache($driver);

Environment Variable Not Set:

bash
# Check environment variable
php -r "echo getenv('CACHE_DRIVER');"

# Set temporarily
export CACHE_DRIVER=redis

# Or in .env file
echo "CACHE_DRIVER=redis" >> .env

Redis Connection Failed:

php
try {
    $cache = cache('redis');
} catch (CacheException $e) {
    // Check Redis configuration
    echo "Redis Host: " . env('REDIS_HOST') . "\n";
    echo "Redis Port: " . env('REDIS_PORT') . "\n";
    echo "Error: " . $e->getMessage() . "\n";
    
    // Fallback to file cache
    $cache = cache('file');
}

Performance Considerations

Instance Reuse:

php
// Efficient: Single instance
$cache = cache();
for ($i = 0; $i < 1000; $i++) {
    $cache->set("key:{$i}", $data, 3600);
}

// Inefficient: Multiple instances
for ($i = 0; $i < 1000; $i++) {
    cache()->set("key:{$i}", $data, 3600);
}

Driver Selection Impact:

php
// Fast: APCu for single server
cache('apcu')->set('key', 'value', 3600);

// Fast: Redis for distributed
cache('redis')->set('key', 'value', 3600);

// Moderate: SQLite
cache('sqlite')->set('key', 'value', 3600);

// Slower: File cache
cache('file')->set('key', 'value', 3600);

The cache() helper function provides a simple, flexible interface to ElliePHP's caching system while automatically handling driver configuration and optimization.

7.10 Cache Statistics

ElliePHP's cache system provides utility methods to monitor cache usage, track statistics, and manage cache entries. These tools help you understand cache behavior and optimize performance.

The count() Method

Get the total number of cached entries.

Signature:

php
public function count(): int

Returns: Total number of cache entries

Usage:

php
use function cache;

$cache = cache();

// Get total cache entries
$count = $cache->count();
echo "Total cached entries: {$count}\n";

// Monitor cache growth
$before = $cache->count();
$cache->set('new:key', 'value', 3600);
$after = $cache->count();

echo "Added " . ($after - $before) . " entries\n";

Monitoring Cache Size:

php
function monitorCacheSize(): void
{
    $cache = cache();
    $count = $cache->count();
    
    if ($count > 10000) {
        report()->warning('Cache has too many entries', [
            'count' => $count
        ]);
        
        // Consider clearing old entries
        $cache->clear();
    }
}

Cache Statistics Dashboard:

php
function getCacheStatistics(): array
{
    $cache = cache();
    
    return [
        'total_entries' => $cache->count(),
        'total_size_bytes' => $cache->size(),
        'total_size_mb' => round($cache->size() / 1024 / 1024, 2),
        'driver' => env('CACHE_DRIVER', 'file'),
        'timestamp' => date('Y-m-d H:i:s')
    ];
}

// Usage
$stats = getCacheStatistics();
print_r($stats);

The size() Method

Get the total size of all cached data in bytes.

Signature:

php
public function size(): int

Returns: Total cache size in bytes

Usage:

php
$cache = cache();

// Get cache size in bytes
$sizeBytes = $cache->size();
echo "Cache size: {$sizeBytes} bytes\n";

// Convert to human-readable format
$sizeKB = round($sizeBytes / 1024, 2);
$sizeMB = round($sizeBytes / 1024 / 1024, 2);
$sizeGB = round($sizeBytes / 1024 / 1024 / 1024, 2);

echo "Cache size: {$sizeKB} KB\n";
echo "Cache size: {$sizeMB} MB\n";
echo "Cache size: {$sizeGB} GB\n";

Size Monitoring:

php
function checkCacheSize(): void
{
    $cache = cache();
    $sizeMB = $cache->size() / 1024 / 1024;
    
    if ($sizeMB > 100) {
        report()->warning('Cache size exceeds 100MB', [
            'size_mb' => round($sizeMB, 2)
        ]);
        
        // Clear cache or implement cleanup strategy
        $cache->clear();
    }
}

Size Tracking:

php
function trackCacheGrowth(): void
{
    $cache = cache();
    
    $before = $cache->size();
    
    // Perform cache operations
    for ($i = 0; $i < 100; $i++) {
        $cache->set("key:{$i}", str_repeat('x', 1000), 3600);
    }
    
    $after = $cache->size();
    $growth = $after - $before;
    
    echo "Cache grew by " . round($growth / 1024, 2) . " KB\n";
}

Helper Function for Human-Readable Size:

php
function formatBytes(int $bytes): string
{
    $units = ['B', 'KB', 'MB', 'GB', 'TB'];
    $power = $bytes > 0 ? floor(log($bytes, 1024)) : 0;
    
    return round($bytes / pow(1024, $power), 2) . ' ' . $units[$power];
}

// Usage
$cache = cache();
$size = $cache->size();
echo "Cache size: " . formatBytes($size) . "\n";

Key Prefixing

All cache keys are automatically prefixed with ellie_cache: to prevent collisions with other applications or systems using the same cache backend.

Automatic Prefixing:

php
$cache = cache();

// You use:
$cache->set('user:123', $data, 3600);

// Actually stored as:
// "ellie_cache:user:123"

// Retrieval is transparent:
$data = $cache->get('user:123'); // Works correctly

Why Key Prefixing?

  1. Namespace Isolation: Prevents conflicts with other applications
  2. Easy Identification: Quickly identify ElliePHP cache entries
  3. Bulk Operations: Clear only ElliePHP cache entries
  4. Multi-Tenant: Run multiple ElliePHP apps on same cache server

Viewing Actual Keys:

bash
# Redis: View all ElliePHP cache keys
redis-cli KEYS "ellie_cache:*"

# Output:
# 1) "ellie_cache:user:123"
# 2) "ellie_cache:product:456"
# 3) "ellie_cache:config:app"

Custom Prefixes (Advanced):

php
// For advanced use cases, you can add additional prefixes
$appPrefix = env('APP_NAME', 'myapp');
$cache = cache();

// Use hierarchical keys
$cache->set("{$appPrefix}:user:123", $data, 3600);
// Stored as: "ellie_cache:myapp:user:123"

Key Validation Rules

Cache keys must follow PSR-16 validation rules:

Valid Key Requirements:

  • Cannot be empty string
  • Maximum 255 characters
  • Cannot contain: {}()/\@:

Valid Keys:

php
$cache = cache();

// Valid keys
$cache->set('user', 'value', 3600);
$cache->set('user:123', 'value', 3600);
$cache->set('user-123', 'value', 3600);
$cache->set('user_123', 'value', 3600);
$cache->set('user.123', 'value', 3600);
$cache->set('user|123', 'value', 3600);

Invalid Keys:

php
use ElliePHP\Components\Cache\Exceptions\InvalidArgumentException;

try {
    // Empty key
    $cache->set('', 'value', 3600);
} catch (InvalidArgumentException $e) {
    echo "Error: Key cannot be empty\n";
}

try {
    // Contains {}
    $cache->set('user:{123}', 'value', 3600);
} catch (InvalidArgumentException $e) {
    echo "Error: Key contains invalid characters\n";
}

try {
    // Contains ()
    $cache->set('user(123)', 'value', 3600);
} catch (InvalidArgumentException $e) {
    echo "Error: Key contains invalid characters\n";
}

try {
    // Contains /
    $cache->set('user/123', 'value', 3600);
} catch (InvalidArgumentException $e) {
    echo "Error: Key contains invalid characters\n";
}

try {
    // Contains @
    $cache->set('user@123', 'value', 3600);
} catch (InvalidArgumentException $e) {
    echo "Error: Key contains invalid characters\n";
}

try {
    // Too long (> 255 characters)
    $cache->set(str_repeat('x', 256), 'value', 3600);
} catch (InvalidArgumentException $e) {
    echo "Error: Key exceeds 255 characters\n";
}

Key Validation Helper:

php
function isValidCacheKey(string $key): bool
{
    // Check length
    if (strlen($key) === 0 || strlen($key) > 255) {
        return false;
    }
    
    // Check for invalid characters
    $invalidChars = ['{}', '()', '/', '\\', '@', ':'];
    foreach ($invalidChars as $char) {
        if (str_contains($key, $char)) {
            return false;
        }
    }
    
    return true;
}

// Usage
if (isValidCacheKey($userKey)) {
    $cache->set($userKey, $data, 3600);
} else {
    throw new InvalidArgumentException("Invalid cache key: {$userKey}");
}

Sanitizing Keys:

php
function sanitizeCacheKey(string $key): string
{
    // Remove invalid characters
    $key = str_replace(['{}', '()', '/', '\\', '@', ':'], '-', $key);
    
    // Trim to 255 characters
    $key = substr($key, 0, 255);
    
    // Ensure not empty
    if (strlen($key) === 0) {
        throw new InvalidArgumentException('Cache key cannot be empty after sanitization');
    }
    
    return $key;
}

// Usage
$userInput = 'user/{123}';
$safeKey = sanitizeCacheKey($userInput); // 'user--123-'
$cache->set($safeKey, $data, 3600);

Complete Monitoring Example

Here's a comprehensive cache monitoring implementation:

php
class CacheMonitor
{
    private Cache $cache;
    
    public function __construct()
    {
        $this->cache = cache();
    }
    
    public function getStatistics(): array
    {
        $count = $this->cache->count();
        $size = $this->cache->size();
        
        return [
            'driver' => env('CACHE_DRIVER', 'file'),
            'entries' => [
                'total' => $count,
                'per_mb' => $size > 0 ? round($count / ($size / 1024 / 1024), 2) : 0
            ],
            'size' => [
                'bytes' => $size,
                'kb' => round($size / 1024, 2),
                'mb' => round($size / 1024 / 1024, 2),
                'gb' => round($size / 1024 / 1024 / 1024, 4)
            ],
            'average_entry_size' => [
                'bytes' => $count > 0 ? round($size / $count, 2) : 0,
                'kb' => $count > 0 ? round($size / $count / 1024, 2) : 0
            ],
            'timestamp' => date('Y-m-d H:i:s')
        ];
    }
    
    public function checkHealth(): array
    {
        $stats = $this->getStatistics();
        $issues = [];
        
        // Check entry count
        if ($stats['entries']['total'] > 10000) {
            $issues[] = 'Too many cache entries (> 10,000)';
        }
        
        // Check size
        if ($stats['size']['mb'] > 100) {
            $issues[] = 'Cache size exceeds 100MB';
        }
        
        // Check average entry size
        if ($stats['average_entry_size']['kb'] > 100) {
            $issues[] = 'Average entry size is large (> 100KB)';
        }
        
        return [
            'healthy' => empty($issues),
            'issues' => $issues,
            'stats' => $stats
        ];
    }
    
    public function cleanup(): array
    {
        $beforeCount = $this->cache->count();
        $beforeSize = $this->cache->size();
        
        // Clear cache
        $this->cache->clear();
        
        $afterCount = $this->cache->count();
        $afterSize = $this->cache->size();
        
        return [
            'entries_removed' => $beforeCount - $afterCount,
            'bytes_freed' => $beforeSize - $afterSize,
            'mb_freed' => round(($beforeSize - $afterSize) / 1024 / 1024, 2)
        ];
    }
    
    public function report(): void
    {
        $stats = $this->getStatistics();
        
        echo "=== Cache Statistics ===\n";
        echo "Driver: {$stats['driver']}\n";
        echo "Total Entries: {$stats['entries']['total']}\n";
        echo "Total Size: {$stats['size']['mb']} MB\n";
        echo "Average Entry Size: {$stats['average_entry_size']['kb']} KB\n";
        echo "Timestamp: {$stats['timestamp']}\n";
        
        $health = $this->checkHealth();
        echo "\n=== Health Check ===\n";
        echo "Status: " . ($health['healthy'] ? 'Healthy' : 'Issues Found') . "\n";
        
        if (!empty($health['issues'])) {
            echo "Issues:\n";
            foreach ($health['issues'] as $issue) {
                echo "  - {$issue}\n";
            }
        }
    }
}

// Usage
$monitor = new CacheMonitor();
$monitor->report();

// Check health
$health = $monitor->checkHealth();
if (!$health['healthy']) {
    // Cleanup if needed
    $result = $monitor->cleanup();
    echo "Cleaned up {$result['entries_removed']} entries, freed {$result['mb_freed']} MB\n";
}

Console Command for Cache Statistics

Create a console command to view cache statistics:

php
use ElliePHP\Components\Console\BaseCommand;

class CacheStatsCommand extends BaseCommand
{
    protected function configure(): void
    {
        $this->setName('cache:stats')
             ->setDescription('Display cache statistics');
    }
    
    protected function handle(): int
    {
        $cache = cache();
        
        $count = $cache->count();
        $size = $cache->size();
        $sizeMB = round($size / 1024 / 1024, 2);
        
        $this->title('Cache Statistics');
        
        $this->table(
            ['Metric', 'Value'],
            [
                ['Driver', env('CACHE_DRIVER', 'file')],
                ['Total Entries', number_format($count)],
                ['Total Size', "{$sizeMB} MB"],
                ['Average Entry Size', $count > 0 ? round($size / $count / 1024, 2) . ' KB' : 'N/A'],
            ]
        );
        
        if ($sizeMB > 100) {
            $this->warning("Cache size exceeds 100MB. Consider running 'cache:clear'");
        }
        
        if ($count > 10000) {
            $this->warning("Cache has over 10,000 entries. Consider cleanup.");
        }
        
        return self::SUCCESS;
    }
}

Usage:

bash
php ellie cache:stats

# Output:
# Cache Statistics
# ================
# 
# Metric              | Value
# --------------------|----------
# Driver              | redis
# Total Entries       | 1,234
# Total Size          | 45.67 MB
# Average Entry Size  | 37.89 KB

Best Practices

1. Monitor Cache Regularly:

php
// Schedule periodic monitoring
function scheduledCacheMonitoring(): void
{
    $cache = cache();
    
    $stats = [
        'count' => $cache->count(),
        'size_mb' => round($cache->size() / 1024 / 1024, 2),
        'timestamp' => time()
    ];
    
    report()->info('Cache statistics', $stats);
    
    // Alert if thresholds exceeded
    if ($stats['size_mb'] > 100) {
        report()->warning('Cache size threshold exceeded', $stats);
    }
}

2. Set Size Limits:

php
function enforceCacheSizeLimit(int $maxSizeMB = 100): void
{
    $cache = cache();
    $sizeMB = $cache->size() / 1024 / 1024;
    
    if ($sizeMB > $maxSizeMB) {
        report()->info('Cache size limit exceeded, clearing cache', [
            'current_size_mb' => round($sizeMB, 2),
            'limit_mb' => $maxSizeMB
        ]);
        
        $cache->clear();
    }
}

3. Track Cache Efficiency:

php
class CacheEfficiencyTracker
{
    private int $hits = 0;
    private int $misses = 0;
    
    public function recordHit(): void
    {
        $this->hits++;
    }
    
    public function recordMiss(): void
    {
        $this->misses++;
    }
    
    public function getHitRate(): float
    {
        $total = $this->hits + $this->misses;
        
        if ($total === 0) {
            return 0.0;
        }
        
        return ($this->hits / $total) * 100;
    }
    
    public function report(): void
    {
        echo "Cache Hits: {$this->hits}\n";
        echo "Cache Misses: {$this->misses}\n";
        echo "Hit Rate: " . round($this->getHitRate(), 2) . "%\n";
    }
}

4. Use Descriptive Keys:

php
// Good: Clear, hierarchical keys
$cache->set('user:123:profile', $profile, 3600);
$cache->set('product:456:details', $details, 3600);
$cache->set('api:github:user:octocat', $data, 300);

// Bad: Unclear keys
$cache->set('u123', $profile, 3600);
$cache->set('p456', $details, 3600);
$cache->set('data', $data, 300);

5. Validate Keys Before Use:

php
function setCacheSafely(string $key, mixed $value, int $ttl): bool
{
    try {
        cache()->set($key, $value, $ttl);
        return true;
    } catch (InvalidArgumentException $e) {
        report()->error('Invalid cache key', [
            'key' => $key,
            'error' => $e->getMessage()
        ]);
        return false;
    }
}

These statistics and utilities help you monitor, optimize, and maintain your cache effectively.


8. Console Commands

ElliePHP provides a powerful console command system built on Symfony Console, allowing you to create CLI tools for your application. The framework includes several built-in commands and makes it easy to create custom commands with rich output formatting and interactive features.

All commands are executed through the ellie CLI script in your project root:

bash
php ellie <command> [options] [arguments]

8.1 Built-in Serve Command

The serve command starts PHP's built-in development server for local development and testing.

Command Syntax

bash
php ellie serve [options]

Options

  • --host - Server host address (default: 127.0.0.1)
  • --port, -p - Server port (default: 8000)
  • --docroot, -d - Document root directory (default: public)

Usage Examples

Start server with default settings:

bash
php ellie serve
# Server starts at http://127.0.0.1:8000

Specify custom host and port:

bash
php ellie serve --host=0.0.0.0 --port=3000
# Server starts at http://0.0.0.0:3000

Use short option syntax:

bash
php ellie serve -p 9000
# Server starts at http://127.0.0.1:9000

Custom document root:

bash
php ellie serve --docroot=dist
# Serves files from the 'dist' directory

Implementation Reference

The serve command is implemented in app/Console/Command/ServeCommand.php. It uses Symfony Process to spawn PHP's built-in server and streams output to the console in real-time.

Key features:

  • Displays server URL and document root on startup
  • Streams server logs to console
  • Runs indefinitely until stopped with Ctrl+C
  • Returns appropriate exit codes on success or failure

8.2 Built-in Cache:Clear Command

The cache:clear command removes cached files from your application's storage directory.

Command Syntax

bash
php ellie cache:clear [options]

Options

  • --config - Clear configuration cache only
  • --routes - Clear routes cache only
  • --views - Clear views cache only
  • --all - Clear all caches

If no options are provided, all caches are cleared by default.

Usage Examples

Clear all caches:

bash
php ellie cache:clear
# or
php ellie cache:clear --all

Clear specific cache:

bash
php ellie cache:clear --routes

Clear multiple specific caches:

bash
php ellie cache:clear --config --routes

Output

The command provides detailed feedback about the clearing process:

Clearing Application Cache
==========================

✓ Config: 3 files removed
✓ Routes: 1 files removed
✓ Views: 0 files removed

[OK] Cache cleared successfully: config, routes, views

Implementation Reference

The cache:clear command is implemented in app/Console/Command/CacheClearCommand.php. It recursively scans cache directories and removes files while preserving .gitignore files.

Cache directories:

  • storage/Cache/config/ - Configuration cache
  • storage/Cache/routes/ - Route cache
  • storage/Cache/views/ - View cache
  • storage/Cache/ - General cache directory

8.3 Built-in Routes Command

The routes command displays all registered routes in your application.

Command Syntax

bash
php ellie routes

This command has no options or arguments.

Usage Example

bash
php ellie routes

Output Format

The command displays routes in a formatted table:

Application Routes
==================

 ---------- ------------ ------------------------- --------
  Method     URI          Handler                   Name
 ---------- ------------ ------------------------- --------
  GET        /            WelcomeController@process  -
  GET        /api/users   UserController@index       users.index
  POST       /api/users   UserController@store       users.store
  GET        /api/test    Closure                    -
 ---------- ------------ ------------------------- --------

[OK] Total routes: 4

Route Information

The table includes:

  • Method - HTTP method (GET, POST, PUT, DELETE, etc.)
  • URI - Route path pattern
  • Handler - Controller class and method, or "Closure" for inline routes
  • Name - Named route identifier, or "-" if unnamed

Implementation Reference

The routes command is implemented in app/Console/Command/RoutesCommand.php. It loads the routes file and extracts route information from the Router's internal collection.

Handler formatting:

  • Class-based routes: ControllerName@methodName
  • Closure routes: Closure
  • String handlers: Displayed as-is

8.4 Built-in Make:Controller Command

The make:controller command generates a new controller class with optional resource methods.

Command Syntax

bash
php ellie make:controller <name> [options]

Arguments

  • name (required) - Controller name (e.g., UserController or just User)

Options

  • --resource, -r - Generate a resource controller with CRUD methods
  • --api - Generate an API resource controller (without create/edit methods)

Usage Examples

Create a basic controller:

bash
php ellie make:controller UserController

Generates a controller with a single process() method:

php
<?php

namespace ElliePHP\Application\Http\Controllers;

use Psr\Http\Message\ResponseInterface;

final readonly class UserController
{
    public function process(): ResponseInterface
    {
        return response()->json([
            'message' => 'Controller action'
        ]);
    }
}

Create a resource controller:

bash
php ellie make:controller PostController --resource

Generates a controller with full CRUD methods:

  • process() - List all resources
  • create() - Show create form
  • store() - Store new resource
  • show($id) - Show single resource
  • edit($id) - Show edit form
  • update($id) - Update resource
  • destroy($id) - Delete resource

Create an API resource controller:

bash
php ellie make:controller ApiUserController --api

Generates a controller with API-focused methods (no create/edit):

  • process() - List all resources
  • show($id) - Show single resource
  • store() - Store new resource
  • update($id) - Update resource
  • destroy($id) - Delete resource

Auto-append "Controller" suffix:

bash
php ellie make:controller User
# Creates UserController.php

Output

[OK] Controller created successfully!
[INFO] Location: app/Http/Controllers/UserController.php

Implementation Reference

The make:controller command is implemented in app/Console/Command/MakeControllerCommand.php.

Key features:

  • Automatically appends "Controller" suffix if not provided
  • Creates the app/Http/Controllers/ directory if it doesn't exist
  • Checks for existing controllers to prevent overwriting
  • Generates proper namespace and use statements
  • Uses readonly class modifier for immutability

8.5 Creating Custom Commands

You can create custom console commands by extending the BaseCommand class.

Basic Command Structure

php
<?php

namespace ElliePHP\Application\Console\Command;

use ElliePHP\Console\Command\BaseCommand;

class MyCustomCommand extends BaseCommand
{
    protected function configure(): void
    {
        $this
            ->setName('my:command')
            ->setDescription('Description of what the command does');
    }

    protected function handle(): int
    {
        $this->info('Command is running...');
        
        // Your command logic here
        
        $this->success('Command completed successfully!');
        
        return self::SUCCESS;
    }
}

Command Methods

configure() - Define command name, description, arguments, and options

handle() - Implement command logic (replaces Symfony's execute() method)

The handle() method should return an exit code:

  • self::SUCCESS (0) - Command succeeded
  • self::FAILURE (1) - Command failed
  • self::INVALID (2) - Invalid usage

Naming Conventions

Commands typically use colon-separated namespaces:

  • make:controller - Generator commands
  • cache:clear - Cache-related commands
  • db:migrate - Database commands
  • queue:work - Queue commands

Use descriptive names that clearly indicate the command's purpose.


8.6 Command Configuration

Configure your command's arguments and options in the configure() method.

Setting Name and Description

php
protected function configure(): void
{
    $this
        ->setName('user:create')
        ->setDescription('Create a new user account');
}

Adding Arguments

Arguments are positional parameters that must be provided in order.

php
use Symfony\Component\Console\Input\InputArgument;

protected function configure(): void
{
    $this
        ->setName('user:create')
        ->setDescription('Create a new user')
        ->addArgument('username', InputArgument::REQUIRED, 'The username')
        ->addArgument('email', InputArgument::REQUIRED, 'The email address')
        ->addArgument('role', InputArgument::OPTIONAL, 'User role', 'user');
}

Argument modes:

  • InputArgument::REQUIRED - Argument must be provided
  • InputArgument::OPTIONAL - Argument is optional
  • InputArgument::IS_ARRAY - Argument accepts multiple values

Accessing arguments:

php
protected function handle(): int
{
    $username = $this->argument('username');
    $email = $this->argument('email');
    $role = $this->argument('role'); // Returns default if not provided
    
    return self::SUCCESS;
}

Adding Options

Options are named parameters that can be provided in any order.

php
use Symfony\Component\Console\Input\InputOption;

protected function configure(): void
{
    $this
        ->setName('user:create')
        ->setDescription('Create a new user')
        ->addOption('admin', 'a', InputOption::VALUE_NONE, 'Create as admin')
        ->addOption('password', 'p', InputOption::VALUE_REQUIRED, 'User password')
        ->addOption('notify', null, InputOption::VALUE_OPTIONAL, 'Send notification', 'email');
}

Option modes:

  • InputOption::VALUE_NONE - Boolean flag (present or not)
  • InputOption::VALUE_REQUIRED - Option requires a value
  • InputOption::VALUE_OPTIONAL - Option value is optional
  • InputOption::VALUE_IS_ARRAY - Option accepts multiple values

Option shortcuts: The second parameter is an optional shortcut (e.g., -a for --admin).

Accessing options:

php
protected function handle(): int
{
    $isAdmin = $this->option('admin'); // true if --admin flag present
    $password = $this->option('password');
    $notify = $this->option('notify');
    
    return self::SUCCESS;
}

Complete Example

php
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

protected function configure(): void
{
    $this
        ->setName('user:create')
        ->setDescription('Create a new user account')
        ->addArgument('username', InputArgument::REQUIRED, 'Username')
        ->addArgument('email', InputArgument::REQUIRED, 'Email address')
        ->addOption('admin', 'a', InputOption::VALUE_NONE, 'Create as admin')
        ->addOption('password', 'p', InputOption::VALUE_OPTIONAL, 'Password')
        ->addOption('force', 'f', InputOption::VALUE_NONE, 'Force creation');
}

Usage:

bash
php ellie user:create john john@example.com --admin -p secret123

8.7 Command Output Methods

The BaseCommand class provides rich output formatting methods.

Status Messages

php
// Success message (green box)
$this->success('Operation completed successfully!');

// Error message (red box)
$this->error('Something went wrong!');

// Info message (blue box)
$this->info('Processing data...');

// Warning message (yellow box)
$this->warning('This action cannot be undone!');

Notes and Comments

php
// Note (highlighted message)
$this->note('Remember to backup your database first.');

// Comment (gray text)
$this->comment('This is a comment or additional information.');

Titles and Sections

php
// Title (large heading with underline)
$this->title('User Management');

// Section (smaller heading)
$this->section('Creating Users');

Tables

Display data in formatted tables:

php
$this->table(
    ['ID', 'Name', 'Email', 'Role'],
    [
        [1, 'John Doe', 'john@example.com', 'Admin'],
        [2, 'Jane Smith', 'jane@example.com', 'User'],
        [3, 'Bob Johnson', 'bob@example.com', 'User'],
    ]
);

Output:

 ---- ------------ ------------------- -------
  ID   Name         Email               Role
 ---- ------------ ------------------- -------
  1    John Doe     john@example.com    Admin
  2    Jane Smith   jane@example.com    User
  3    Bob Johnson  bob@example.com     User
 ---- ------------ ------------------- -------

Simple Output

php
// Write a line with newline
$this->line('This is a simple line of text.');

// Write without newline
$this->write('Processing');
$this->write('.');
$this->write('.');
$this->write('.');
$this->line(' Done!');

Complete Example

php
protected function handle(): int
{
    $this->title('Database Migration');
    
    $this->section('Running Migrations');
    $this->info('Starting migration process...');
    
    // Simulate migration
    $migrations = [
        ['2024_01_01_create_users_table', 'Completed'],
        ['2024_01_02_create_posts_table', 'Completed'],
        ['2024_01_03_add_roles_column', 'Completed'],
    ];
    
    $this->table(
        ['Migration', 'Status'],
        $migrations
    );
    
    $this->success('All migrations completed successfully!');
    $this->note('Remember to clear the cache.');
    
    return self::SUCCESS;
}

8.8 Interactive Commands

Create interactive commands that prompt users for input.

Ask for Input

php
protected function handle(): int
{
    // Simple question
    $name = $this->ask('What is your name?');
    
    // Question with default value
    $email = $this->ask('What is your email?', 'user@example.com');
    
    $this->info("Hello, $name! Your email is $email");
    
    return self::SUCCESS;
}

Confirm Action

php
protected function handle(): int
{
    // Confirmation (default: true)
    $confirmed = $this->confirm('Do you want to continue?');
    
    // Confirmation (default: false)
    $delete = $this->confirm('Delete all data?', false);
    
    if ($delete) {
        $this->warning('Deleting all data...');
        // Perform deletion
    } else {
        $this->info('Operation cancelled.');
    }
    
    return self::SUCCESS;
}

Multiple Choice

php
protected function handle(): int
{
    $environment = $this->choice(
        'Select environment',
        ['development', 'staging', 'production'],
        'development' // default
    );
    
    $this->info("Selected environment: $environment");
    
    return self::SUCCESS;
}

Complete Interactive Example

php
<?php

namespace ElliePHP\Application\Console\Command;

use ElliePHP\Console\Command\BaseCommand;

class UserCreateCommand extends BaseCommand
{
    protected function configure(): void
    {
        $this
            ->setName('user:create')
            ->setDescription('Create a new user interactively');
    }

    protected function handle(): int
    {
        $this->title('Create New User');
        
        // Gather user information
        $username = $this->ask('Username');
        $email = $this->ask('Email address');
        
        $role = $this->choice(
            'Select role',
            ['user', 'moderator', 'admin'],
            'user'
        );
        
        // Confirm creation
        $this->section('User Details');
        $this->table(
            ['Field', 'Value'],
            [
                ['Username', $username],
                ['Email', $email],
                ['Role', $role],
            ]
        );
        
        $confirmed = $this->confirm('Create this user?');
        
        if (!$confirmed) {
            $this->warning('User creation cancelled.');
            return self::SUCCESS;
        }
        
        // Create user (simulated)
        $this->info('Creating user...');
        
        // Your user creation logic here
        
        $this->success("User '$username' created successfully!");
        
        return self::SUCCESS;
    }
}

Usage:

bash
php ellie user:create

The command will interactively prompt for all required information.


8.9 Command Registration

Register your custom commands in the configs/Commands.php configuration file.

Configuration Structure

php
<?php

use ElliePHP\Framework\Application\Console\Command\CacheClearCommand;
use ElliePHP\Framework\Application\Console\Command\MakeControllerCommand;
use ElliePHP\Framework\Application\Console\Command\RoutesCommand;
use ElliePHP\Framework\Application\Console\Command\ServeCommand;

return [
    'app' => [
        // Built-in commands
        MakeControllerCommand::class,
        CacheClearCommand::class,
        ServeCommand::class,
        RoutesCommand::class,
        
        // Your custom commands
        // Add your command classes here
    ]
];

Registering Custom Commands

Simply add your command class to the app array:

php
<?php

use ElliePHP\Application\Console\Command\UserCreateCommand;
use ElliePHP\Application\Console\Command\DatabaseBackupCommand;
use ElliePHP\Application\Console\Command\ReportGenerateCommand;

return [
    'app' => [
        // Built-in commands
        MakeControllerCommand::class,
        CacheClearCommand::class,
        ServeCommand::class,
        RoutesCommand::class,
        
        // Custom commands
        UserCreateCommand::class,
        DatabaseBackupCommand::class,
        ReportGenerateCommand::class,
    ]
];

Command Discovery

Once registered, your commands are automatically available through the ellie CLI:

bash
php ellie list
# Shows all registered commands

php ellie user:create
# Runs your custom command

Dependency Injection

Commands registered in Commands.php have access to the dependency injection container. You can inject dependencies through the constructor:

php
<?php

namespace ElliePHP\Application\Console\Command;

use ElliePHP\Console\Command\BaseCommand;
use ElliePHP\Application\Services\UserService;

class UserCreateCommand extends BaseCommand
{
    public function __construct(
        private readonly UserService $userService
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this->setName('user:create')
             ->setDescription('Create a new user');
    }

    protected function handle(): int
    {
        // Use injected service
        $user = $this->userService->create([
            'username' => $this->ask('Username'),
            'email' => $this->ask('Email'),
        ]);
        
        $this->success("User created with ID: {$user->id}");
        
        return self::SUCCESS;
    }
}

The container will automatically resolve and inject the UserService dependency.


8.10 Command Exit Codes

Commands should return appropriate exit codes to indicate success or failure.

Standard Exit Codes

The BaseCommand class provides three standard exit code constants:

php
// Success (exit code 0)
return self::SUCCESS;

// Failure (exit code 1)
return self::FAILURE;

// Invalid usage (exit code 2)
return self::INVALID;

When to Use Each Code

self::SUCCESS (0)

Use when the command completes successfully:

php
protected function handle(): int
{
    $this->info('Processing data...');
    
    // Successful operation
    $result = $this->processData();
    
    $this->success('Data processed successfully!');
    return self::SUCCESS;
}

self::FAILURE (1)

Use when the command encounters an error or fails to complete:

php
protected function handle(): int
{
    $file = $this->argument('file');
    
    if (!file_exists($file)) {
        $this->error("File not found: $file");
        return self::FAILURE;
    }
    
    try {
        $this->processFile($file);
        return self::SUCCESS;
    } catch (\Exception $e) {
        $this->error("Failed to process file: {$e->getMessage()}");
        return self::FAILURE;
    }
}

self::INVALID (2)

Use when the command is invoked with invalid arguments or options:

php
protected function handle(): int
{
    $count = (int) $this->option('count');
    
    if ($count < 1 || $count > 100) {
        $this->error('Count must be between 1 and 100');
        return self::INVALID;
    }
    
    // Process with valid count
    return self::SUCCESS;
}

Exit Code Usage in Scripts

Exit codes are useful when commands are called from shell scripts:

bash
#!/bin/bash

php ellie user:create john john@example.com

if [ $? -eq 0 ]; then
    echo "User created successfully"
else
    echo "Failed to create user"
    exit 1
fi

Complete Example

php
<?php

namespace ElliePHP\Application\Console\Command;

use ElliePHP\Console\Command\BaseCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

class FileProcessCommand extends BaseCommand
{
    protected function configure(): void
    {
        $this
            ->setName('file:process')
            ->setDescription('Process a file')
            ->addArgument('file', InputArgument::REQUIRED, 'File path')
            ->addOption('format', 'f', InputOption::VALUE_REQUIRED, 'Output format');
    }

    protected function handle(): int
    {
        $file = $this->argument('file');
        $format = $this->option('format');
        
        // Validate format option
        $validFormats = ['json', 'xml', 'csv'];
        if ($format && !in_array($format, $validFormats)) {
            $this->error("Invalid format: $format");
            $this->comment('Valid formats: ' . implode(', ', $validFormats));
            return self::INVALID;
        }
        
        // Check file exists
        if (!file_exists($file)) {
            $this->error("File not found: $file");
            return self::FAILURE;
        }
        
        // Process file
        try {
            $this->info("Processing file: $file");
            
            // Your processing logic here
            $result = $this->processFile($file, $format);
            
            $this->success('File processed successfully!');
            $this->info("Output: $result");
            
            return self::SUCCESS;
            
        } catch (\Exception $e) {
            $this->error("Processing failed: {$e->getMessage()}");
            return self::FAILURE;
        }
    }
    
    private function processFile(string $file, ?string $format): string
    {
        // Processing implementation
        return 'output.txt';
    }
}

Usage examples:

bash
# Success
php ellie file:process data.txt --format=json
# Exit code: 0

# Invalid format
php ellie file:process data.txt --format=invalid
# Exit code: 2

# File not found
php ellie file:process missing.txt
# Exit code: 1

9. Logging

ElliePHP provides a robust logging system built on Monolog, fully compliant with PSR-3 logging standards. The framework offers multiple log channels, structured logging with context parameters, and automatic exception tracking to help you monitor and debug your applications effectively.

9.1 Logging Basics

ElliePHP's logging system is built on Monolog, a powerful and flexible logging library that implements the PSR-3 LoggerInterface standard. This ensures compatibility with other PSR-3 compliant libraries and provides a consistent logging API across your application.

PSR-3 Compliance

The framework's Log class acts as a facade for PSR-3 compliant loggers, providing a simple and intuitive interface for logging messages at various severity levels. All log methods accept a message string and an optional context array for structured data.

Log Channels

ElliePHP uses two separate log channels to organize different types of log entries:

  1. App Channel (app.log) - For general application logging including:

    • Debug information
    • Informational messages
    • Warnings
    • General errors
    • HTTP request analytics
  2. Exceptions Channel (exceptions.log) - Dedicated to exception logging:

    • Critical errors
    • Uncaught exceptions
    • Exception stack traces
    • Exception context and metadata

This separation makes it easier to monitor critical issues separately from general application logs.

Log File Locations

By default, all log files are stored in the storage/Logs/ directory:

  • Application logs: storage/Logs/app.log
  • Exception logs: storage/Logs/exceptions.log

These paths are configured using the storage_logs_path() helper function, which ensures consistent path resolution across your application.

Monolog Integration

The framework initializes Monolog loggers with StreamHandler instances that write to file streams. Each logger is configured with appropriate log levels:

  • The app logger accepts all levels from DEBUG and above
  • The exception logger is configured for CRITICAL level messages
php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Psr\Log\LogLevel;

// App logger configuration
$appLogger = new Logger('app');
$appLogger->pushHandler(
    new StreamHandler(storage_logs_path('app.log'), LogLevel::DEBUG)
);

// Exception logger configuration
$exceptionLogger = new Logger('exceptions');
$exceptionLogger->pushHandler(
    new StreamHandler(storage_logs_path('exceptions.log'), LogLevel::CRITICAL)
);

9.2 Log Levels

ElliePHP supports all standard PSR-3 log levels, each designed for specific types of messages. Understanding when to use each level helps maintain meaningful and actionable logs.

debug() - Debug Information

Use for detailed diagnostic information useful during development and troubleshooting.

php
report()->debug('User query executed', [
    'query' => 'SELECT * FROM users WHERE id = ?',
    'params' => [123],
    'execution_time' => 0.045
]);

When to use:

  • Detailed variable dumps
  • Query execution details
  • Internal state information
  • Development-only diagnostics

info() - Informational Messages

Use for general informational messages that highlight application progress and significant events.

php
report()->info('User logged in successfully', [
    'user_id' => 456,
    'ip_address' => '192.168.1.1',
    'timestamp' => time()
]);

report()->info('HTTP Request Analytics', [
    'method' => 'GET',
    'uri' => '/api/users',
    'status' => 200,
    'duration_ms' => 45.23
]);

When to use:

  • User actions (login, logout, registration)
  • HTTP request/response analytics
  • Background job completion
  • System state changes

warning() - Warning Messages

Use for exceptional occurrences that are not errors but may require attention.

php
report()->warning('Cache miss for frequently accessed key', [
    'key' => 'user_preferences_123',
    'fallback' => 'database'
]);

report()->warning('API rate limit approaching', [
    'current_requests' => 950,
    'limit' => 1000,
    'window' => '1 hour'
]);

When to use:

  • Deprecated feature usage
  • Performance degradation
  • Resource limits approaching
  • Recoverable errors

error() - Error Messages

Use for runtime errors that don't require immediate action but should be logged and monitored.

php
report()->error('Failed to send email notification', [
    'recipient' => 'user@example.com',
    'error' => 'SMTP connection timeout',
    'retry_count' => 3
]);

report()->error('External API request failed', [
    'api' => 'payment_gateway',
    'endpoint' => '/api/v1/charge',
    'status_code' => 503
]);

When to use:

  • Failed external API calls
  • Database connection errors
  • File system errors
  • Failed background jobs

critical() - Critical Conditions

Use for critical conditions that require immediate attention, such as system component failures.

php
report()->critical('Database connection pool exhausted', [
    'active_connections' => 100,
    'max_connections' => 100,
    'waiting_requests' => 25
]);

report()->critical('Disk space critically low', [
    'available_mb' => 50,
    'threshold_mb' => 100,
    'partition' => '/var/log'
]);

When to use:

  • System component unavailability
  • Data corruption detected
  • Security breaches
  • Resource exhaustion

9.3 Context Parameters

All logging methods accept an optional context array as the second parameter. Context parameters allow you to attach structured data to log entries, making logs more searchable, filterable, and actionable.

Basic Context Usage

Context parameters are passed as an associative array:

php
report()->info('Order processed', [
    'order_id' => 'ORD-12345',
    'customer_id' => 789,
    'total_amount' => 99.99,
    'currency' => 'USD',
    'payment_method' => 'credit_card'
]);

Structured Data in Context

You can include complex data structures in context arrays:

php
report()->info('Product search performed', [
    'search_term' => 'laptop',
    'filters' => [
        'price_min' => 500,
        'price_max' => 1500,
        'brand' => ['Dell', 'HP', 'Lenovo']
    ],
    'results_count' => 42,
    'page' => 1,
    'sort_by' => 'price_asc'
]);

Correlation ID Tracking

Correlation IDs help trace requests across multiple services and log entries. The LoggingMiddleware automatically generates and propagates correlation IDs:

php
// In LoggingMiddleware
$correlationId = $request->getHeaderLine('X-Correlation-ID')
    ?: bin2hex(random_bytes(8));

report()->info('HTTP Request Analytics', [
    'correlation_id' => $correlationId,
    'method' => $request->getMethod(),
    'uri' => (string) $request->getUri(),
    'status' => $response->getStatusCode()
]);

You can use correlation IDs in your application code:

php
$correlationId = request()->getAttribute('correlation_id');

report()->info('Processing payment', [
    'correlation_id' => $correlationId,
    'order_id' => $orderId,
    'amount' => $amount
]);

// Later in the same request flow
report()->info('Payment completed', [
    'correlation_id' => $correlationId,
    'transaction_id' => $transactionId
]);

LoggingMiddleware Example

The built-in LoggingMiddleware demonstrates comprehensive context usage for HTTP request analytics:

php
report()->info('HTTP Request Analytics', [
    'correlation_id' => $correlationId,
    'method' => $request->getMethod(),
    'uri' => (string) $request->getUri(),
    'status' => $response->getStatusCode(),
    'route' => $routeName,
    
    'duration_ms' => $durationMs,
    'memory_delta_bytes' => $memoryDelta,
    
    'request_size_bytes' => $requestSize,
    'response_size_bytes' => $responseSize,
    
    'client_ip' => $request->getServerParams()['REMOTE_ADDR'] ?? null,
    'user_agent' => $request->getHeaderLine('User-Agent'),
]);

This creates rich, queryable log entries that help with performance monitoring, debugging, and analytics.

Best Practices for Context

  1. Use consistent keys - Standardize context key names across your application
  2. Include identifiers - Always include relevant IDs (user_id, order_id, etc.)
  3. Add timestamps - Include timestamps for time-sensitive operations
  4. Avoid sensitive data - Never log passwords, tokens, or PII
  5. Keep it relevant - Only include context that adds value to the log entry

9.4 Exception Logging

ElliePHP provides automatic exception logging through a dedicated exception channel and the exception() method, which captures comprehensive exception details including stack traces and metadata.

The exception() Method

The exception() method is specifically designed for logging exceptions with full context:

php
try {
    // Some operation that might fail
    $result = $paymentGateway->charge($amount);
} catch (PaymentException $e) {
    report()->exception($e);
    
    // Handle the exception
    return response()->json([
        'error' => 'Payment processing failed'
    ], 500);
}

Automatic Exception Context

When you call exception(), the Log class automatically captures:

  • Exception message - The error message
  • Exception type - The fully qualified class name
  • File and line - Where the exception was thrown
  • Stack trace - Complete call stack as a string
  • Exception object - The full exception for detailed inspection
php
// Inside the Log class
public function exception(Throwable $exception): void
{
    $context = [
        'exception' => $exception,
        'file' => $exception->getFile(),
        'line' => $exception->getLine(),
        'trace' => $exception->getTraceAsString(),
        'type' => get_class($exception),
    ];

    $this->exceptionLogger->critical($exception->getMessage(), $context);
}

Automatic Exception Channel Routing

Exceptions logged via exception() are automatically routed to the dedicated exceptions channel (storage/Logs/exceptions.log), keeping critical errors separate from general application logs.

Kernel Exception Handling

The framework's Kernel class automatically logs all uncaught exceptions:

php
// In Kernel.php
private function handleException(Throwable $e): void
{
    report()->exception($e);
    
    // ... handle the exception response
}

This ensures that even unexpected exceptions are captured in your logs, providing a complete audit trail of application errors.

Exception Logging Examples

Basic exception logging:

php
try {
    $user = User::findOrFail($id);
} catch (ModelNotFoundException $e) {
    report()->exception($e);
    return response()->notFound();
}

With additional context:

php
try {
    $data = $externalApi->fetch($endpoint);
} catch (ApiException $e) {
    // Log the exception
    report()->exception($e);
    
    // Also log additional context
    report()->error('External API call failed', [
        'endpoint' => $endpoint,
        'status_code' => $e->getStatusCode(),
        'retry_count' => $retryCount
    ]);
    
    throw $e;
}

Catching and re-throwing:

php
try {
    $this->processOrder($order);
} catch (Throwable $e) {
    report()->exception($e);
    
    // Re-throw to let higher-level handlers deal with it
    throw new OrderProcessingException(
        'Failed to process order: ' . $e->getMessage(),
        previous: $e
    );
}

9.5 Report Helper Function

The report() helper function provides convenient access to the logging system from anywhere in your application. It returns a singleton instance of the Log class, ensuring consistent logger configuration throughout your application.

Basic Usage

php
// Get the logger instance
$logger = report();

// Log a message
report()->info('Application started');

Logger Instance

The report() function returns a Log instance configured with two Monolog loggers:

php
function report(): Log
{
    static $instance = null;

    if ($instance === null) {
        $appLogger = new Logger('app');
        $appLogger->pushHandler(
            new StreamHandler(storage_logs_path('app.log'), LogLevel::DEBUG)
        );

        $exceptionLogger = new Logger('exceptions');
        $exceptionLogger->pushHandler(
            new StreamHandler(storage_logs_path('exceptions.log'), LogLevel::CRITICAL)
        );

        $instance = new Log($appLogger, $exceptionLogger);
    }

    return $instance;
}

Practical Examples

In controllers:

php
namespace ElliePHP\Framework\Application\Http\Controllers;

use ElliePHP\Components\Support\Http\Response;

class UserController
{
    public function store(): Response
    {
        report()->info('Creating new user', [
            'email' => request()->input('email'),
            'ip_address' => request()->getServerParams()['REMOTE_ADDR']
        ]);
        
        // Create user logic...
        
        report()->info('User created successfully', [
            'user_id' => $user->id
        ]);
        
        return response()->json($user, 201);
    }
}

In services:

php
namespace ElliePHP\Framework\Application\Services;

class PaymentService
{
    public function processPayment(int $orderId, float $amount): bool
    {
        report()->info('Processing payment', [
            'order_id' => $orderId,
            'amount' => $amount
        ]);
        
        try {
            $result = $this->gateway->charge($amount);
            
            report()->info('Payment successful', [
                'order_id' => $orderId,
                'transaction_id' => $result->id
            ]);
            
            return true;
        } catch (PaymentException $e) {
            report()->exception($e);
            return false;
        }
    }
}

In middleware:

php
namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class AuthMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $token = $request->getHeaderLine('Authorization');
        
        if (empty($token)) {
            report()->warning('Unauthorized access attempt', [
                'uri' => (string) $request->getUri(),
                'ip' => $request->getServerParams()['REMOTE_ADDR']
            ]);
            
            return response()->unauthorized();
        }
        
        // Validate token...
        
        return $handler->handle($request);
    }
}

In console commands:

php
namespace ElliePHP\Framework\Application\Console\Command;

use ElliePHP\Components\Console\BaseCommand;

class DataImportCommand extends BaseCommand
{
    protected function handle(): int
    {
        report()->info('Starting data import');
        
        try {
            $count = $this->importData();
            
            report()->info('Data import completed', [
                'records_imported' => $count
            ]);
            
            return self::SUCCESS;
        } catch (\Exception $e) {
            report()->exception($e);
            return self::FAILURE;
        }
    }
}

9.6 Log Configuration

ElliePHP's logging system is configured through the report() helper function, but you can customize log handlers, formatters, and behavior to suit your application's needs.

Default Configuration

The default configuration creates two StreamHandler instances that write to files:

php
// App logger - logs all levels from DEBUG and above
$appLogger = new Logger('app');
$appLogger->pushHandler(
    new StreamHandler(storage_logs_path('app.log'), LogLevel::DEBUG)
);

// Exception logger - logs CRITICAL level messages
$exceptionLogger = new Logger('exceptions');
$exceptionLogger->pushHandler(
    new StreamHandler(storage_logs_path('exceptions.log'), LogLevel::CRITICAL)
);

Log File Paths

Log files are stored in the storage/Logs/ directory by default. The paths are resolved using the storage_logs_path() helper:

php
storage_logs_path('app.log')        // storage/Logs/app.log
storage_logs_path('exceptions.log') // storage/Logs/exceptions.log

You can create additional log files for specific purposes:

php
storage_logs_path('security.log')   // storage/Logs/security.log
storage_logs_path('performance.log') // storage/Logs/performance.log

Log Level Filtering

Each handler can be configured with a minimum log level. Only messages at or above this level will be logged:

php
use Psr\Log\LogLevel;

// Only log WARNING and above
$handler = new StreamHandler(
    storage_logs_path('warnings.log'),
    LogLevel::WARNING
);

Available log levels (from lowest to highest severity):

  • LogLevel::DEBUG
  • LogLevel::INFO
  • LogLevel::NOTICE
  • LogLevel::WARNING
  • LogLevel::ERROR
  • LogLevel::CRITICAL
  • LogLevel::ALERT
  • LogLevel::EMERGENCY

Custom Handlers

Monolog supports various handlers beyond file logging. You can add custom handlers to the logger:

Rotating file handler:

php
use Monolog\Handler\RotatingFileHandler;

$handler = new RotatingFileHandler(
    storage_logs_path('app.log'),
    30, // Keep 30 days of logs
    LogLevel::DEBUG
);

$appLogger->pushHandler($handler);

Email handler for critical errors:

php
use Monolog\Handler\NativeMailerHandler;

$mailHandler = new NativeMailerHandler(
    'admin@example.com',
    'Critical Error in Application',
    'noreply@example.com',
    LogLevel::CRITICAL
);

$appLogger->pushHandler($mailHandler);

Slack handler for notifications:

php
use Monolog\Handler\SlackWebhookHandler;

$slackHandler = new SlackWebhookHandler(
    'https://hooks.slack.com/services/YOUR/WEBHOOK/URL',
    '#alerts',
    'ElliePHP Bot',
    true,
    null,
    true,
    true,
    LogLevel::ERROR
);

$appLogger->pushHandler($slackHandler);

Custom Formatters

You can customize how log messages are formatted:

php
use Monolog\Formatter\JsonFormatter;
use Monolog\Formatter\LineFormatter;

// JSON format for machine parsing
$jsonFormatter = new JsonFormatter();
$handler->setFormatter($jsonFormatter);

// Custom line format
$lineFormatter = new LineFormatter(
    "[%datetime%] %channel%.%level_name%: %message% %context%\n",
    "Y-m-d H:i:s"
);
$handler->setFormatter($lineFormatter);

Environment-Based Configuration

You can adjust logging behavior based on the environment:

php
$logLevel = env('APP_ENV') === 'production' 
    ? LogLevel::WARNING 
    : LogLevel::DEBUG;

$handler = new StreamHandler(
    storage_logs_path('app.log'),
    $logLevel
);

Multiple Handlers

You can push multiple handlers to a single logger to log to different destinations:

php
$appLogger = new Logger('app');

// Always log to file
$appLogger->pushHandler(
    new StreamHandler(storage_logs_path('app.log'), LogLevel::DEBUG)
);

// Also log errors to a separate file
$appLogger->pushHandler(
    new StreamHandler(storage_logs_path('errors.log'), LogLevel::ERROR)
);

// Send critical errors via email
$appLogger->pushHandler(
    new NativeMailerHandler(
        'admin@example.com',
        'Critical Error',
        'noreply@example.com',
        LogLevel::CRITICAL
    )
);

Processors

Monolog processors add extra information to log records:

php
use Monolog\Processor\IntrospectionProcessor;
use Monolog\Processor\MemoryUsageProcessor;
use Monolog\Processor\WebProcessor;

// Add file, line, class, and method info
$appLogger->pushProcessor(new IntrospectionProcessor());

// Add memory usage info
$appLogger->pushProcessor(new MemoryUsageProcessor());

// Add web request info (IP, URL, method)
$appLogger->pushProcessor(new WebProcessor());

Custom Log Channels

You can create additional log channels for specific purposes:

php
// Security audit log
$securityLogger = new Logger('security');
$securityLogger->pushHandler(
    new StreamHandler(storage_logs_path('security.log'), LogLevel::INFO)
);

// Performance monitoring log
$performanceLogger = new Logger('performance');
$performanceLogger->pushHandler(
    new StreamHandler(storage_logs_path('performance.log'), LogLevel::DEBUG)
);

// Create a custom Log instance
$customLog = new Log($securityLogger, $exceptionLogger);

10. Utilities & Helpers

10.1 String Utilities

ElliePHP provides a comprehensive Str utility class for string manipulation, validation, and transformation. All methods are static and can be called directly without instantiation.

Case Conversion Methods

toCamelCase()

Convert a string to camelCase format:

php
use ElliePHP\Components\Support\Util\Str;

Str::toCamelCase('hello-world');        // 'helloWorld'
Str::toCamelCase('hello_world');        // 'helloWorld'
Str::toCamelCase('hello world');        // 'helloWorld'

toPascalCase()

Convert a string to PascalCase format:

php
Str::toPascalCase('hello-world');       // 'HelloWorld'
Str::toPascalCase('hello_world');       // 'HelloWorld'

toSnakeCase()

Convert a string to snake_case format:

php
Str::toSnakeCase('HelloWorld');         // 'hello_world'
Str::toSnakeCase('helloWorld');         // 'hello_world'
Str::toSnakeCase('hello world');        // 'hello_world'

toKebabCase()

Convert a string to kebab-case format:

php
Str::toKebabCase('HelloWorld');         // 'hello-world'
Str::toKebabCase('helloWorld');         // 'hello-world'
Str::toKebabCase('hello world');        // 'hello-world'

slug()

Create a URL-friendly slug from a string:

php
Str::slug('Hello World!');              // 'hello-world'
Str::slug('Product #123');              // 'product-123'
Str::slug('Hello World', '_');          // 'hello_world'

String Operation Methods

startsWith()

Check if a string starts with a given substring:

php
Str::startsWith('Hello World', 'Hello');    // true
Str::startsWith('Hello World', 'World');    // false

startsWithAny()

Check if a string starts with any of the given substrings:

php
Str::startsWithAny('Hello World', ['Hi', 'Hello']);  // true
Str::startsWithAny('Hello World', ['Hi', 'Hey']);    // false

endsWith()

Check if a string ends with a given substring:

php
Str::endsWith('Hello World', 'World');      // true
Str::endsWith('Hello World', 'Hello');      // false

endsWithAny()

Check if a string ends with any of the given substrings:

php
Str::endsWithAny('file.php', ['.php', '.js']);  // true

contains()

Check if a string contains a given substring:

php
Str::contains('Hello World', 'World');      // true
Str::contains('Hello World', 'Goodbye');    // false

containsAny()

Check if a string contains any of the given substrings:

php
Str::containsAny('Hello World', ['World', 'Universe']);  // true

containsAll()

Check if a string contains all of the given substrings:

php
Str::containsAll('Hello World', ['Hello', 'World']);  // true
Str::containsAll('Hello World', ['Hello', 'Goodbye']); // false

limit()

Limit the length of a string with an optional ending:

php
Str::limit('This is a long string', 10);           // 'This is a ...'
Str::limit('This is a long string', 10, '---');    // 'This is a ---'
Str::limit('Short', 10);                           // 'Short'

truncateWords()

Truncate a string to a specific number of words:

php
Str::truncateWords('The quick brown fox jumps', 3);  // 'The quick brown...'

random()

Generate a random alphanumeric string:

php
Str::random();          // Random 16-character string
Str::random(32);        // Random 32-character string
// Example: 'aB3xY9mK2pQ7wR5t'

replace()

Replace all occurrences of a search string:

php
Str::replace('world', 'universe', 'Hello world');  // 'Hello universe'

replaceFirst()

Replace the first occurrence of a search string:

php
Str::replaceFirst('a', 'X', 'banana');  // 'bXnana'

replaceLast()

Replace the last occurrence of a search string:

php
Str::replaceLast('a', 'X', 'banana');   // 'bananX'

before()

Get the portion of a string before a given value:

php
Str::before('user@example.com', '@');   // 'user'

after()

Get the portion of a string after a given value:

php
Str::after('user@example.com', '@');    // 'example.com'

beforeLast()

Get the portion before the last occurrence:

php
Str::beforeLast('path/to/file.txt', '/');  // 'path/to'

afterLast()

Get the portion after the last occurrence:

php
Str::afterLast('path/to/file.txt', '/');   // 'file.txt'

substr()

Extract a substring:

php
Str::substr('Hello World', 0, 5);       // 'Hello'
Str::substr('Hello World', 6);          // 'World'

repeat()

Repeat a string multiple times:

php
Str::repeat('*', 5);                    // '*****'

mask()

Mask a portion of a string:

php
Str::mask('4111111111111111', '*', 4, 8);  // '4111********1111'
Str::mask('secret', '*');                   // '******'

extractStringBetween()

Extract text between two delimiters:

php
Str::extractStringBetween('Hello [World]', '[', ']');  // 'World'

Validation Methods

isEmail()

Validate if a string is a valid email address:

php
Str::isEmail('user@example.com');       // true
Str::isEmail('invalid-email');          // false

isUrl()

Validate if a string is a valid URL:

php
Str::isUrl('https://example.com');      // true
Str::isUrl('not-a-url');                // false

isJson()

Validate if a string is valid JSON:

php
Str::isJson('{"name":"John"}');         // true
Str::isJson('invalid json');            // false

isAlphanumeric()

Check if string contains only alphanumeric characters:

php
Str::isAlphanumeric('abc123');          // true
Str::isAlphanumeric('abc-123');         // false

isAlpha()

Check if string contains only alphabetic characters:

php
Str::isAlpha('abcdef');                 // true
Str::isAlpha('abc123');                 // false

isNumeric()

Check if string is numeric:

php
Str::isNumeric('12345');                // true
Str::isNumeric('123.45');               // true
Str::isNumeric('abc');                  // false

isEmpty()

Check if a string is empty (after trimming):

php
Str::isEmpty('   ');                    // true
Str::isEmpty('Hello');                  // false

isNotEmpty()

Check if a string is not empty:

php
Str::isNotEmpty('Hello');               // true
Str::isNotEmpty('   ');                 // false

Additional String Methods

length()

Get the length of a string (multibyte safe):

php
Str::length('Hello');                   // 5
Str::length('こんにちは');              // 5

wordCount()

Count the number of words in a string:

php
Str::wordCount('Hello World');          // 2

words()

Get the first N words from a string:

php
Str::words('The quick brown fox', 2);   // 'The quick'

clean()

Remove special characters, keeping only letters, numbers, and spaces:

php
Str::clean('Hello! @World#');           // 'Hello World'

trim(), ltrim(), rtrim()

Remove whitespace from strings:

php
Str::trim('  Hello  ');                 // 'Hello'
Str::ltrim('  Hello');                  // 'Hello'
Str::rtrim('Hello  ');                  // 'Hello'

removePrefix()

Remove a prefix from a string:

php
Str::removePrefix('HelloWorld', 'Hello');  // 'World'

removeSuffix()

Remove a suffix from a string:

php
Str::removeSuffix('HelloWorld', 'World');  // 'Hello'

ensurePrefix()

Ensure a string starts with a prefix:

php
Str::ensurePrefix('World', 'Hello');    // 'HelloWorld'
Str::ensurePrefix('HelloWorld', 'Hello'); // 'HelloWorld'

ensureSuffix()

Ensure a string ends with a suffix:

php
Str::ensureSuffix('Hello', 'World');    // 'HelloWorld'
Str::ensureSuffix('HelloWorld', 'World'); // 'HelloWorld'

toArray()

Convert a string to an array of characters:

php
Str::toArray('Hello');                  // ['H', 'e', 'l', 'l', 'o']

split()

Split a string by a delimiter:

php
Str::split(',', 'a,b,c');               // ['a', 'b', 'c']
Str::split(',', 'a,b,c', 2);            // ['a', 'b,c']

swap()

Swap keywords in a string with values:

php
Str::swap('Hello {name}!', ['{name}' => 'John']);  // 'Hello John!'

match()

Execute a regular expression match:

php
$matches = Str::match('/\d+/', 'Order 123');
// ['123']

matchAll()

Execute a global regular expression match:

php
$matches = Str::matchAll('/\d+/', 'Order 123 and 456');
// [['123', '456']]

plural()

Convert a word to plural form (basic English rules):

php
Str::plural('user');                    // 'users'
Str::plural('child');                   // 'children'
Str::plural('person');                  // 'people'

singular()

Convert a word to singular form:

php
Str::singular('users');                 // 'user'
Str::singular('children');              // 'child'

Practical Examples

Building a URL Slug

php
$title = 'My First Blog Post!';
$slug = Str::slug($title);
// 'my-first-blog-post'

Masking Sensitive Data

php
$creditCard = '4111111111111111';
$masked = Str::mask($creditCard, '*', 4, 8);
// '4111********1111'

$email = 'john.doe@example.com';
$username = Str::before($email, '@');
$maskedEmail = Str::mask($username, '*', 2) . '@' . Str::after($email, '@');
// 'jo******@example.com'

Validating User Input

php
$email = request()->input('email');
if (!Str::isEmail($email)) {
    return response()->json(['error' => 'Invalid email'], 400);
}

$url = request()->input('website');
if (!Str::isEmpty($url) && !Str::isUrl($url)) {
    return response()->json(['error' => 'Invalid URL'], 400);
}

Generating Random Tokens

php
$apiKey = Str::random(32);
$verificationCode = Str::random(6);

Text Processing

php
$text = 'The quick brown fox jumps over the lazy dog';

// Limit for preview
$preview = Str::limit($text, 20);  // 'The quick brown fox ...'

// Extract first few words
$summary = Str::words($text, 4);   // 'The quick brown fox'

// Count words
$wordCount = Str::wordCount($text); // 9

10.2 File Utilities

The File utility class provides a comprehensive set of methods for file and directory operations. All methods are static and handle errors gracefully with exceptions.

Read/Write Methods

get()

Read the contents of a file:

php
use ElliePHP\Components\Support\Util\File;

$content = File::get('/path/to/file.txt');
// Returns file contents as string
// Throws RuntimeException if file doesn't exist or can't be read

put()

Write contents to a file (creates or overwrites):

php
File::put('/path/to/file.txt', 'Hello World');
// Returns number of bytes written

// With file locking
File::put('/path/to/file.txt', 'Hello World', true);

append()

Append contents to the end of a file:

php
File::append('/path/to/log.txt', "New log entry\n");
// Returns number of bytes written
// Creates file if it doesn't exist

prepend()

Prepend contents to the beginning of a file:

php
File::prepend('/path/to/file.txt', "Header\n");
// Returns number of bytes written

File Information Methods

exists()

Check if a file or directory exists:

php
if (File::exists('/path/to/file.txt')) {
    // File exists
}

isFile()

Check if a path is a file:

php
if (File::isFile('/path/to/file.txt')) {
    // It's a file, not a directory
}

isDirectory()

Check if a path is a directory:

php
if (File::isDirectory('/path/to/directory')) {
    // It's a directory
}

size()

Get the file size in bytes:

php
$bytes = File::size('/path/to/file.txt');
// Returns integer (file size in bytes)

humanSize()

Get human-readable file size:

php
$size = File::humanSize('/path/to/file.txt');
// '1.5 MB', '256 KB', '3.2 GB', etc.

$size = File::humanSize('/path/to/file.txt', 3);
// With 3 decimal places: '1.523 MB'

mimeType()

Get the MIME type of a file:

php
$mime = File::mimeType('/path/to/image.jpg');
// 'image/jpeg'

$mime = File::mimeType('/path/to/document.pdf');
// 'application/pdf'

extension()

Get the file extension:

php
$ext = File::extension('/path/to/file.txt');
// 'txt'

name()

Get the file name without extension:

php
$name = File::name('/path/to/file.txt');
// 'file'

basename()

Get the file name with extension:

php
$basename = File::basename('/path/to/file.txt');
// 'file.txt'

dirname()

Get the directory name of a path:

php
$dir = File::dirname('/path/to/file.txt');
// '/path/to'

lastModified()

Get the last modification time as Unix timestamp:

php
$timestamp = File::lastModified('/path/to/file.txt');
// 1699564800

$date = date('Y-m-d H:i:s', $timestamp);
// '2024-11-17 10:30:00'

isOlderThan()

Check if a file is older than a given number of seconds:

php
if (File::isOlderThan('/path/to/cache.txt', 3600)) {
    // File is older than 1 hour
}

File Permission Methods

isReadable()

Check if a file is readable:

php
if (File::isReadable('/path/to/file.txt')) {
    $content = File::get('/path/to/file.txt');
}

isWritable()

Check if a file is writable:

php
if (File::isWritable('/path/to/file.txt')) {
    File::put('/path/to/file.txt', 'New content');
}

permissions()

Get file permissions:

php
$perms = File::permissions('/path/to/file.txt');
// Returns integer (e.g., 33188 for 0644)

chmod()

Set file permissions:

php
File::chmod('/path/to/file.txt', 0644);
File::chmod('/path/to/script.sh', 0755);

JSON File Methods

json()

Read and decode a JSON file:

php
$data = File::json('/path/to/config.json');
// Returns associative array

$data = File::json('/path/to/config.json', false);
// Returns object

putJson()

Encode and write data to a JSON file:

php
$data = ['name' => 'John', 'age' => 30];
File::putJson('/path/to/data.json', $data);
// Pretty-printed by default

File::putJson('/path/to/data.json', $data, 0);
// Compact JSON (no pretty print)

File Operation Methods

copy()

Copy a file to a new location:

php
File::copy('/path/to/source.txt', '/path/to/destination.txt');
// Returns true on success

move()

Move a file to a new location:

php
File::move('/path/to/old.txt', '/path/to/new.txt');
// Returns true on success

delete()

Delete a file:

php
File::delete('/path/to/file.txt');
// Returns true on success
// Returns true if file doesn't exist (idempotent)

replace()

Replace content in a file:

php
File::replace('/path/to/file.txt', 'old text', 'new text');
// Returns number of bytes written

replaceRegex()

Replace content using regular expressions:

php
File::replaceRegex('/path/to/file.txt', '/\d+/', 'NUMBER');
// Replaces all numbers with 'NUMBER'

contains()

Check if a file contains a string:

php
if (File::contains('/path/to/file.txt', 'search term')) {
    // File contains the search term
}

hash()

Generate a hash of file contents:

php
$hash = File::hash('/path/to/file.txt');
// SHA256 hash by default

$hash = File::hash('/path/to/file.txt', 'md5');
// MD5 hash

Directory Methods

makeDirectory()

Create a directory:

php
File::makeDirectory('/path/to/new/directory');
// Creates nested directories by default with 0755 permissions

File::makeDirectory('/path/to/directory', 0777, false);
// Non-recursive, custom permissions

files()

Get all files in a directory:

php
$files = File::files('/path/to/directory');
// Returns array of file paths

$files = File::files('/path/to/directory', true);
// Recursive search

directories()

Get all subdirectories in a directory:

php
$dirs = File::directories('/path/to/directory');
// Returns array of directory paths

deleteDirectory()

Delete a directory and its contents:

php
File::deleteDirectory('/path/to/directory');
// Deletes directory and all contents

File::deleteDirectory('/path/to/directory', true);
// Preserves the directory, only deletes contents

cleanDirectory()

Remove all contents from a directory:

php
File::cleanDirectory('/path/to/directory');
// Removes all files and subdirectories, keeps the directory itself

copyDirectory()

Copy an entire directory:

php
File::copyDirectory('/path/to/source', '/path/to/destination');
// Recursively copies all files and subdirectories

moveDirectory()

Move an entire directory:

php
File::moveDirectory('/path/to/source', '/path/to/destination');
// Copies then deletes source

Advanced Methods

lines()

Get file contents as an array of lines:

php
$lines = File::lines('/path/to/file.txt');
// Returns array of lines

$lines = File::lines('/path/to/file.txt', true);
// Skip empty lines

glob()

Find files matching a pattern:

php
$files = File::glob('/path/to/*.txt');
// Returns array of matching file paths

$files = File::glob('/path/to/**/*.php');
// Recursive pattern matching

matchesPattern()

Check if a path matches a pattern:

php
if (File::matchesPattern('*.txt', 'file.txt')) {
    // Matches
}

relativePath()

Get relative path from one file to another:

php
$relative = File::relativePath('/var/www/app', '/var/www/public');
// '../public'

ensureExists()

Ensure a file exists, create if it doesn't:

php
$created = File::ensureExists('/path/to/file.txt', 'Initial content');
// Returns true if created, false if already existed

closestExistingDirectory()

Find the closest existing parent directory:

php
$dir = File::closestExistingDirectory('/path/that/may/not/exist');
// Returns '/path/that' if '/path/that/may/not/exist' doesn't exist

Practical Examples

Reading Configuration Files

php
// Read JSON configuration
$config = File::json(root_path('config/app.json'));

// Read plain text configuration
$content = File::get(root_path('.env'));
$lines = File::lines(root_path('.env'), true); // Skip empty lines

Writing Log Files

php
$logFile = storage_logs_path('custom.log');
$timestamp = date('Y-m-d H:i:s');
$message = "[{$timestamp}] User logged in\n";

File::append($logFile, $message);

Managing Uploads

php
$uploadedFile = $_FILES['document']['tmp_name'];
$destination = storage_path('uploads/' . uniqid() . '.pdf');

// Ensure upload directory exists
File::makeDirectory(File::dirname($destination));

// Move uploaded file
File::move($uploadedFile, $destination);

// Get file info
$size = File::humanSize($destination);
$mime = File::mimeType($destination);

Cache File Management

php
$cacheFile = storage_cache_path('data.cache');

// Check if cache is fresh
if (File::exists($cacheFile) && !File::isOlderThan($cacheFile, 3600)) {
    $data = unserialize(File::get($cacheFile));
} else {
    $data = fetchDataFromDatabase();
    File::put($cacheFile, serialize($data));
}

Directory Cleanup

php
// Clean old log files
$logDir = storage_logs_path();
$files = File::files($logDir);

foreach ($files as $file) {
    if (File::isOlderThan($file, 30 * 86400)) { // 30 days
        File::delete($file);
    }
}

Backup Operations

php
$source = storage_path('database.sqlite');
$backup = storage_path('backups/database-' . date('Y-m-d') . '.sqlite');

// Ensure backup directory exists
File::makeDirectory(File::dirname($backup));

// Create backup
File::copy($source, $backup);

// Verify backup
$sourceHash = File::hash($source);
$backupHash = File::hash($backup);

if ($sourceHash === $backupHash) {
    report()->info('Backup created successfully');
}

Template Processing

php
$template = File::get(app_path('templates/email.html'));
$template = str_replace('{name}', $userName, $template);
$template = str_replace('{date}', date('Y-m-d'), $template);

File::put(storage_path('emails/processed.html'), $template);

10.3 JSON Utilities

The Json utility class provides robust JSON encoding, decoding, and manipulation with comprehensive error handling. All methods throw JsonException on errors when using strict mode.

Encode/Decode Methods with Error Handling

encode()

Encode a value to JSON string with error handling:

php
use ElliePHP\Components\Support\Util\Json;

$data = ['name' => 'John', 'age' => 30];
$json = Json::encode($data);
// '{"name":"John","age":30}'

// Throws JsonException on error
try {
    $json = Json::encode($invalidData);
} catch (JsonException $e) {
    report()->error('JSON encoding failed: ' . $e->getMessage());
}

decode()

Decode a JSON string with error handling:

php
$json = '{"name":"John","age":30}';
$data = Json::decode($json);
// ['name' => 'John', 'age' => 30]

$data = Json::decode($json, false);
// Returns object instead of associative array

// Throws JsonException on invalid JSON
try {
    $data = Json::decode('invalid json');
} catch (JsonException $e) {
    report()->error('JSON decoding failed');
}

safeEncode()

Encode with safe handling (returns null on error):

php
$json = Json::safeEncode($data);
// Returns JSON string or null on error

if ($json === null) {
    // Encoding failed
}

safeDecode()

Decode with safe handling (returns null on error):

php
$data = Json::safeDecode($json);
// Returns decoded data or null on error

if ($data === null) {
    // Decoding failed or JSON was literally null
}

isValid()

Validate if a string is valid JSON:

php
if (Json::isValid('{"name":"John"}')) {
    // Valid JSON
}

if (!Json::isValid('invalid json')) {
    // Invalid JSON
}

lastError()

Get the last JSON error message:

php
$data = Json::safeDecode('invalid json');
if ($data === null) {
    $error = Json::lastError();
    // 'Syntax error'
}

lastErrorCode()

Get the last JSON error code:

php
$code = Json::lastErrorCode();
// JSON_ERROR_NONE, JSON_ERROR_SYNTAX, etc.

Pretty Printing

pretty()

Format JSON with pretty printing:

php
$data = ['name' => 'John', 'age' => 30, 'city' => 'New York'];
$json = Json::pretty($data);
/*
{
    "name": "John",
    "age": 30,
    "city": "New York"
}
*/

format()

Format an existing JSON string:

php
$compact = '{"name":"John","age":30}';
$pretty = Json::format($compact);
// Pretty-printed version

minify()

Remove unnecessary whitespace from JSON:

php
$pretty = '{
    "name": "John",
    "age": 30
}';
$compact = Json::minify($pretty);
// '{"name":"John","age":30}'

Dot Notation Methods

get()

Get a value from JSON using dot notation:

php
$json = '{"user":{"name":"John","address":{"city":"NYC"}}}';

$name = Json::get($json, 'user.name');
// 'John'

$city = Json::get($json, 'user.address.city');
// 'NYC'

$default = Json::get($json, 'user.phone', 'N/A');
// 'N/A' (default value)

set()

Set a value in JSON using dot notation:

php
$json = '{"user":{"name":"John"}}';
$json = Json::set($json, 'user.age', 30);
// '{"user":{"name":"John","age":30}}'

$json = Json::set($json, 'user.address.city', 'NYC');
// Creates nested structure

has()

Check if a key exists using dot notation:

php
$json = '{"user":{"name":"John"}}';

if (Json::has($json, 'user.name')) {
    // Key exists
}

if (!Json::has($json, 'user.age')) {
    // Key doesn't exist
}

forget()

Remove a key from JSON using dot notation:

php
$json = '{"user":{"name":"John","age":30}}';
$json = Json::forget($json, 'user.age');
// '{"user":{"name":"John"}}'

File Methods

fromFile()

Read and decode JSON from a file:

php
$data = Json::fromFile('/path/to/config.json');
// Returns associative array

$data = Json::fromFile('/path/to/config.json', false);
// Returns object

// Throws JsonException if file doesn't exist or JSON is invalid

toFile()

Encode and write JSON to a file:

php
$data = ['name' => 'John', 'age' => 30];

Json::toFile('/path/to/data.json', $data);
// Writes compact JSON

Json::toFile('/path/to/data.json', $data, true);
// Writes pretty-printed JSON

// Throws JsonException on error

Utility Methods

merge()

Merge multiple JSON strings or arrays:

php
$json1 = '{"name":"John"}';
$json2 = '{"age":30}';
$merged = Json::merge($json1, $json2);
// ['name' => 'John', 'age' => 30]

// Can also merge arrays
$merged = Json::merge(['a' => 1], ['b' => 2]);
// ['a' => 1, 'b' => 2]

mergeDeep()

Deep merge multiple JSON strings or arrays:

php
$json1 = '{"user":{"name":"John","age":30}}';
$json2 = '{"user":{"age":31,"city":"NYC"}}';
$merged = Json::mergeDeep($json1, $json2);
// ['user' => ['name' => 'John', 'age' => 31, 'city' => 'NYC']]

flatten()

Flatten a nested JSON structure:

php
$json = '{"user":{"name":"John","address":{"city":"NYC"}}}';
$flat = Json::flatten($json);
// '{"user.name":"John","user.address.city":"NYC"}'

$flat = Json::flatten($json, '_');
// Custom separator: '{"user_name":"John","user_address_city":"NYC"}'

only()

Extract specific keys from JSON:

php
$json = '{"name":"John","age":30,"city":"NYC"}';
$filtered = Json::only($json, ['name', 'age']);
// '{"name":"John","age":30}'

except()

Remove specific keys from JSON:

php
$json = '{"name":"John","age":30,"city":"NYC"}';
$filtered = Json::except($json, ['age']);
// '{"name":"John","city":"NYC"}'

validate()

Validate JSON against a basic schema:

php
$json = '{"name":"John","age":30}';
$schema = [
    'name' => ['type' => 'string', 'required' => true],
    'age' => ['type' => 'integer', 'required' => true],
];

if (Json::validate($json, $schema)) {
    // JSON is valid
}

Conversion Methods

toXml()

Convert JSON to XML:

php
$json = '{"user":{"name":"John","age":30}}';
$xml = Json::toXml($json, 'root');
/*
<?xml version="1.0"?>
<root>
    <user>
        <name>John</name>
        <age>30</age>
    </user>
</root>
*/

toCsv()

Convert JSON array to CSV:

php
$json = '[
    {"name":"John","age":30},
    {"name":"Jane","age":25}
]';
$csv = Json::toCsv($json);
/*
name,age
John,30
Jane,25
*/

$csv = Json::toCsv($json, false);
// Without headers

Practical Examples

API Response Handling

php
// Encode API response
$data = [
    'status' => 'success',
    'data' => $users,
    'meta' => ['total' => count($users)]
];

return response()
    ->json($data)
    ->withHeader('Content-Type', 'application/json');

// Or using Json utility for more control
$json = Json::pretty($data);
return response($json, 200)
    ->withHeader('Content-Type', 'application/json');

Configuration Management

php
// Read configuration
$config = Json::fromFile(root_path('config/app.json'));

// Update configuration
$config = Json::set(
    Json::encode($config),
    'database.host',
    'localhost'
);

// Save configuration
Json::toFile(root_path('config/app.json'), Json::decode($config), true);

Data Transformation

php
// Flatten nested data for export
$nested = [
    'user' => [
        'profile' => ['name' => 'John', 'age' => 30],
        'settings' => ['theme' => 'dark']
    ]
];

$flat = Json::flatten(Json::encode($nested));
// {"user.profile.name":"John","user.profile.age":30,"user.settings.theme":"dark"}

Safe JSON Processing

php
// Process user input safely
$input = request()->input('data');

if (!Json::isValid($input)) {
    return response()->json(['error' => 'Invalid JSON'], 400);
}

$data = Json::safeDecode($input);
if ($data === null) {
    return response()->json(['error' => 'Failed to parse JSON'], 400);
}

// Process $data...

Merging Configuration Files

php
// Load base configuration
$base = Json::fromFile(root_path('config/base.json'));

// Load environment-specific configuration
$env = Json::fromFile(root_path('config/' . env('APP_ENV') . '.json'));

// Deep merge configurations
$config = Json::mergeDeep(
    Json::encode($base),
    Json::encode($env)
);

// Use merged configuration
$finalConfig = Json::decode($config);

Data Export

php
// Export users to CSV
$users = [
    ['name' => 'John', 'email' => 'john@example.com', 'age' => 30],
    ['name' => 'Jane', 'email' => 'jane@example.com', 'age' => 25],
];

$csv = Json::toCsv(Json::encode($users));

return response($csv, 200)
    ->withHeader('Content-Type', 'text/csv')
    ->withHeader('Content-Disposition', 'attachment; filename="users.csv"');

Nested Data Access

php
// Access deeply nested API response
$response = '{"data":{"user":{"profile":{"name":"John"}}}}';

$name = Json::get($response, 'data.user.profile.name', 'Unknown');
// 'John'

// Check if nested key exists
if (Json::has($response, 'data.user.profile.email')) {
    $email = Json::get($response, 'data.user.profile.email');
}

10.4 Hash Utilities

The Hash utility class provides secure password hashing, cryptographic hashing, and unique ID generation. All methods are static and use industry-standard algorithms.

Password Hashing

create()

Hash a password using bcrypt:

php
use ElliePHP\Components\Support\Util\Hash;

$password = 'secret123';
$hash = Hash::create($password);
// '$2y$12$...' (bcrypt hash)

// With custom cost factor
$hash = Hash::create($password, ['rounds' => 14]);
// Higher cost = more secure but slower

check()

Verify a password against a hash:

php
$password = 'secret123';
$hash = '$2y$12$...';

if (Hash::check($password, $hash)) {
    // Password is correct
} else {
    // Password is incorrect
}

needsRehash()

Check if a hash needs to be rehashed (e.g., cost factor changed):

php
$hash = '$2y$10$...'; // Old hash with cost 10

if (Hash::needsRehash($hash, ['rounds' => 12])) {
    // Rehash with new cost factor
    $newHash = Hash::create($password, ['rounds' => 12]);
    // Update database with new hash
}

info()

Get information about a password hash:

php
$info = Hash::info($hash);
/*
[
    'algo' => 1,           // PASSWORD_BCRYPT
    'algoName' => 'bcrypt',
    'options' => ['cost' => 12]
]
*/

argon2i()

Hash using Argon2i algorithm:

php
$hash = Hash::argon2i($password);
// Argon2i hash

$hash = Hash::argon2i($password, [
    'memory_cost' => 2048,
    'time_cost' => 4,
    'threads' => 3
]);

argon2id()

Hash using Argon2id algorithm (recommended):

php
$hash = Hash::argon2id($password);
// Argon2id hash (most secure)

Hash Algorithms

sha256()

Generate a SHA256 hash:

php
$hash = Hash::sha256('Hello World');
// 'a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e'

sha512()

Generate a SHA512 hash:

php
$hash = Hash::sha512('Hello World');
// Longer hash string

md5()

Generate an MD5 hash (not recommended for security):

php
$hash = Hash::md5('Hello World');
// 'b10a8db164e0754105b7a99be72e3fe5'

sha1()

Generate a SHA1 hash:

php
$hash = Hash::sha1('Hello World');
// '0a4d55a8d778e5022fab701977c5d840bbc486d0'

xxh3()

Generate an XXH3 hash (fast, non-cryptographic):

php
$hash = Hash::xxh3('Hello World');
// Fast hash for checksums and hash tables

crc32()

Generate a CRC32 hash:

php
$hash = Hash::crc32('Hello World');
// Fast checksum

hash()

Generate a hash using any algorithm:

php
$hash = Hash::hash('Hello World', 'sha256');
$hash = Hash::hash('Hello World', 'sha512');
$hash = Hash::hash('Hello World', 'md5');

// Get binary output
$binary = Hash::hash('Hello World', 'sha256', true);

algorithms()

Get list of available hash algorithms:

php
$algos = Hash::algorithms();
// ['md5', 'sha1', 'sha256', 'sha512', 'xxh3', ...]

HMAC Hashing

hmac()

Generate an HMAC hash with a secret key:

php
$message = 'Important data';
$secretKey = 'my-secret-key';

$hmac = Hash::hmac($message, $secretKey);
// HMAC-SHA256 by default

$hmac = Hash::hmac($message, $secretKey, 'sha512');
// HMAC-SHA512

File Hashing

file()

Generate a hash of a file:

php
$hash = Hash::file('/path/to/file.txt');
// SHA256 hash of file contents

$hash = Hash::file('/path/to/file.txt', 'md5');
// MD5 hash of file

// Returns false if file doesn't exist
if ($hash === false) {
    // File not found
}

ID Generation

uuid()

Generate a UUID v4:

php
$uuid = Hash::uuid();
// '550e8400-e29b-41d4-a716-446655440000'

// Use for unique identifiers
$userId = Hash::uuid();
$orderId = Hash::uuid();

ulid()

Generate a ULID (Universally Unique Lexicographically Sortable Identifier):

php
$ulid = Hash::ulid();
// '01ARZ3NDEKTSV4RRFFQ69G5FAV'

// ULIDs are sortable by creation time
$id1 = Hash::ulid();
sleep(1);
$id2 = Hash::ulid();
// $id1 < $id2 (lexicographically)

nanoid()

Generate a Nanoid:

php
$id = Hash::nanoid();
// 'V1StGXR8_Z5jdHi6B-myT' (21 characters by default)

$id = Hash::nanoid(10);
// 'V1StGXR8_Z' (10 characters)

$id = Hash::nanoid(21, '0123456789');
// Custom alphabet (numbers only)

random()

Generate a random hash:

php
$token = Hash::random();
// 32-character random hex string

$token = Hash::random(64);
// 64-character random hex string

// Use for API tokens, session IDs, etc.

Comparison and Validation

equals()

Compare two strings in constant time (prevents timing attacks):

php
$known = 'secret-token-123';
$user = request()->header('X-API-Token');

if (Hash::equals($known, $user)) {
    // Tokens match
}

checksum()

Generate a checksum for data integrity:

php
$data = 'Important data';
$checksum = Hash::checksum($data);
// SHA256 checksum

$checksum = Hash::checksum($data, 'md5');
// MD5 checksum

verifyChecksum()

Verify a checksum:

php
$data = 'Important data';
$checksum = Hash::checksum($data);

if (Hash::verifyChecksum($data, $checksum)) {
    // Data integrity verified
}

Encoding Methods

base64()

Generate a base64-encoded hash:

php
$hash = Hash::base64('Hello World');
// Base64-encoded SHA256 hash

base64Url()

Generate a URL-safe base64-encoded hash:

php
$hash = Hash::base64Url('Hello World');
// URL-safe base64 (no +, /, or = characters)

short()

Generate a short hash (for URLs, etc.):

php
$hash = Hash::short('Hello World');
// 'a591a6d4' (8 characters by default)

$hash = Hash::short('Hello World', 12);
// 'a591a6d40bf4' (12 characters)

salted()

Generate a hash with salt:

php
$salt = Hash::random(16);
$hash = Hash::salted('password', $salt);
// Hash with salt prepended

Practical Examples

User Authentication

php
// Registration
$password = request()->input('password');
$hash = Hash::create($password);

// Store $hash in database
$user = new User();
$user->password = $hash;
$user->save();

// Login
$password = request()->input('password');
$user = User::findByEmail(request()->input('email'));

if (Hash::check($password, $user->password)) {
    // Password correct, log in user
    
    // Check if rehash needed
    if (Hash::needsRehash($user->password)) {
        $user->password = Hash::create($password);
        $user->save();
    }
} else {
    // Password incorrect
}

API Token Generation

php
// Generate API token
$token = Hash::random(32);

// Store hashed version in database
$hashedToken = Hash::sha256($token);
$user->api_token = $hashedToken;
$user->save();

// Return plain token to user (only time they see it)
return response()->json(['token' => $token]);

// Verify token on subsequent requests
$providedToken = request()->bearerToken();
$hashedProvided = Hash::sha256($providedToken);

if (Hash::equals($user->api_token, $hashedProvided)) {
    // Token valid
}

File Integrity Verification

php
// Generate checksum when uploading
$uploadedFile = '/path/to/uploaded/file.pdf';
$checksum = Hash::file($uploadedFile);

// Store checksum in database
$document = new Document();
$document->file_path = $uploadedFile;
$document->checksum = $checksum;
$document->save();

// Verify integrity later
$currentChecksum = Hash::file($document->file_path);

if (!Hash::verifyChecksum($document->file_path, $document->checksum, 'sha256')) {
    report()->warning('File integrity compromised', [
        'file' => $document->file_path,
        'expected' => $document->checksum,
        'actual' => $currentChecksum
    ]);
}

Unique Identifiers

php
// Generate unique order ID
$orderId = Hash::ulid();
// '01ARZ3NDEKTSV4RRFFQ69G5FAV'

// Generate unique file name
$fileName = Hash::uuid() . '.jpg';
// '550e8400-e29b-41d4-a716-446655440000.jpg'

// Generate short URL code
$urlCode = Hash::short($originalUrl, 8);
// 'a591a6d4'

HMAC Signatures

php
// Sign API request
$payload = json_encode(['user_id' => 123, 'action' => 'update']);
$secretKey = env('API_SECRET');
$signature = Hash::hmac($payload, $secretKey);

// Send with request
$headers = [
    'X-Signature' => $signature,
    'Content-Type' => 'application/json'
];

// Verify signature on receiving end
$receivedPayload = request()->getBody()->getContents();
$receivedSignature = request()->header('X-Signature');
$expectedSignature = Hash::hmac($receivedPayload, $secretKey);

if (!Hash::equals($expectedSignature, $receivedSignature)) {
    return response()->json(['error' => 'Invalid signature'], 401);
}

Password Reset Tokens

php
// Generate reset token
$token = Hash::random(32);
$hashedToken = Hash::sha256($token);

// Store hashed token with expiry
$user->reset_token = $hashedToken;
$user->reset_expires = time() + 3600; // 1 hour
$user->save();

// Send plain token via email
sendEmail($user->email, 'Reset link: /reset?token=' . $token);

// Verify token
$providedToken = request()->input('token');
$hashedProvided = Hash::sha256($providedToken);

if (Hash::equals($user->reset_token, $hashedProvided) && 
    time() < $user->reset_expires) {
    // Token valid, allow password reset
}

Data Deduplication

php
// Check for duplicate content
$content = request()->input('content');
$contentHash = Hash::sha256($content);

$existing = Document::where('content_hash', $contentHash)->first();

if ($existing) {
    return response()->json([
        'error' => 'Duplicate content',
        'existing_id' => $existing->id
    ], 409);
}

// Store new document with hash
$document = new Document();
$document->content = $content;
$document->content_hash = $contentHash;
$document->save();

10.5 Environment Utilities

The Env class provides robust environment variable management with automatic type casting, validation, and .env file loading. It uses the popular vlucas/phpdotenv library under the hood.

Env Class Instantiation and Loading

Constructor

Create a new Env instance:

php
use ElliePHP\Components\Support\Util\Env;

// Load from default .env file
$env = new Env('/path/to/project');

// Load from custom file(s)
$env = new Env('/path/to/project', '.env.local');
$env = new Env('/path/to/project', ['.env', '.env.local']);

load()

Load environment variables from .env file:

php
$env = new Env(root_path());
$env->load();

// Variables are now available via $env->get()

loadWithRequired()

Load environment variables and require specific variables:

php
$env = new Env(root_path());
$env->loadWithRequired(['APP_NAME', 'APP_ENV', 'APP_DEBUG']);

// Throws exception if any required variable is missing

isLoaded()

Check if environment variables have been loaded:

php
if (!$env->isLoaded()) {
    $env->load();
}

Get Method with Automatic Type Casting

get()

Get an environment variable with automatic type casting:

php
// Basic usage
$appName = $env->get('APP_NAME');
// Returns string value

// With default value
$debug = $env->get('APP_DEBUG', false);
// Returns false if APP_DEBUG not set

Type Casting Based on Default Value

The get() method automatically casts the return value to match the type of the default value:

php
// String (default)
$name = $env->get('APP_NAME', 'MyApp');
// Returns string

// Boolean
$debug = $env->get('APP_DEBUG', false);
// Returns boolean (true/false)

// Integer
$port = $env->get('APP_PORT', 8000);
// Returns integer

// Float
$timeout = $env->get('TIMEOUT', 5.5);
// Returns float

// Null (no casting)
$optional = $env->get('OPTIONAL_VAR', null);
// Returns value as-is or null

Smart Casting for Booleans, Integers, Floats

When no default value is provided, the get() method uses smart casting to determine the appropriate type:

Boolean Values

The following strings are automatically converted to boolean:

php
// In .env file:
// APP_DEBUG=true
// FEATURE_ENABLED=false
// MAINTENANCE_MODE=yes
// CACHE_ENABLED=no
// SSL_ENABLED=on
// API_ENABLED=off
// STRICT_MODE=1
// RELAXED_MODE=0

$debug = $env->get('APP_DEBUG');        // true (boolean)
$feature = $env->get('FEATURE_ENABLED'); // false (boolean)
$maintenance = $env->get('MAINTENANCE_MODE'); // true (boolean)
$cache = $env->get('CACHE_ENABLED');    // false (boolean)
$ssl = $env->get('SSL_ENABLED');        // true (boolean)
$api = $env->get('API_ENABLED');        // false (boolean)
$strict = $env->get('STRICT_MODE');     // true (boolean)
$relaxed = $env->get('RELAXED_MODE');   // false (boolean)

Integer Values

Numeric strings without decimals are cast to integers:

php
// In .env file:
// APP_PORT=8000
// MAX_CONNECTIONS=100

$port = $env->get('APP_PORT');          // 8000 (integer)
$maxConn = $env->get('MAX_CONNECTIONS'); // 100 (integer)

Float Values

Numeric strings with decimals are cast to floats:

php
// In .env file:
// TIMEOUT=5.5
// TAX_RATE=0.15

$timeout = $env->get('TIMEOUT');        // 5.5 (float)
$taxRate = $env->get('TAX_RATE');       // 0.15 (float)

String Values

Non-numeric, non-boolean values remain as strings:

php
// In .env file:
// APP_NAME=ElliePHP
// DATABASE_HOST=localhost

$name = $env->get('APP_NAME');          // 'ElliePHP' (string)
$host = $env->get('DATABASE_HOST');     // 'localhost' (string)

Special .env Values

The Env class recognizes special literal values in .env files:

null Values

php
// In .env file:
// OPTIONAL_VALUE=null
// ANOTHER_VALUE=(null)

$value = $env->get('OPTIONAL_VALUE');   // null
$another = $env->get('ANOTHER_VALUE');  // null

Empty String Values

php
// In .env file:
// EMPTY_VALUE=empty
// ANOTHER_EMPTY=(empty)

$value = $env->get('EMPTY_VALUE');      // '' (empty string)
$another = $env->get('ANOTHER_EMPTY');  // '' (empty string)

Boolean Literals

php
// In .env file:
// EXPLICIT_TRUE=(true)
// EXPLICIT_FALSE=(false)

$value = $env->get('EXPLICIT_TRUE');    // true (boolean)
$another = $env->get('EXPLICIT_FALSE'); // false (boolean)

Quoted Values

Values in quotes are always treated as strings:

php
// In .env file:
// QUOTED_NUMBER="123"
// QUOTED_BOOL="true"

$number = $env->get('QUOTED_NUMBER');   // '123' (string, not integer)
$bool = $env->get('QUOTED_BOOL');       // 'true' (string, not boolean)

Has Method

has()

Check if an environment variable exists:

php
if ($env->has('APP_DEBUG')) {
    $debug = $env->get('APP_DEBUG');
}

if (!$env->has('OPTIONAL_FEATURE')) {
    // Variable not set
}

Require Methods

require()

Require specific environment variables to be set:

php
$env->load();
$env->require(['APP_NAME', 'APP_ENV']);

// Throws exception if any variable is missing

requireNotEmpty()

Require variables to be set and not empty:

php
$env->load();
$env->requireNotEmpty(['DATABASE_HOST', 'DATABASE_NAME']);

// Throws exception if any variable is missing or empty

requireOneOf()

Require a variable to be one of specific values:

php
$env->load();
$env->requireOneOf('APP_ENV', ['local', 'staging', 'production']);

// Throws exception if APP_ENV is not one of the allowed values

All Method

all()

Get all environment variables:

php
$allVars = $env->all();
// Returns associative array of all environment variables

Practical Examples

Basic Environment Setup

php
// In bootstrap or configuration file
$env = new Env(root_path());
$env->load();

// Access variables with type safety
$appName = $env->get('APP_NAME', 'ElliePHP');
$debug = $env->get('APP_DEBUG', false);
$port = $env->get('APP_PORT', 8000);

Required Configuration Validation

php
$env = new Env(root_path());

try {
    $env->loadWithRequired([
        'APP_NAME',
        'APP_ENV',
        'DATABASE_HOST',
        'DATABASE_NAME'
    ]);
} catch (Exception $e) {
    die('Missing required environment variables: ' . $e->getMessage());
}

Environment-Specific Configuration

php
$env = new Env(root_path());
$env->load();

// Validate environment
$env->requireOneOf('APP_ENV', ['local', 'staging', 'production']);

$appEnv = $env->get('APP_ENV');

if ($appEnv === 'production') {
    // Production-specific settings
    $env->requireNotEmpty(['APP_KEY', 'DATABASE_PASSWORD']);
}

Type-Safe Configuration

php
$env = new Env(root_path());
$env->load();

// Boolean configuration
$debug = $env->get('APP_DEBUG', false);
$maintenance = $env->get('MAINTENANCE_MODE', false);

if ($debug) {
    ini_set('display_errors', '1');
}

// Numeric configuration
$maxConnections = $env->get('MAX_CONNECTIONS', 100);
$timeout = $env->get('TIMEOUT', 30.0);

// String configuration
$timezone = $env->get('APP_TIMEZONE', 'UTC');
date_default_timezone_set($timezone);

Database Configuration

php
$env = new Env(root_path());
$env->load();

$config = [
    'host' => $env->get('DB_HOST', 'localhost'),
    'port' => $env->get('DB_PORT', 3306),
    'database' => $env->get('DB_DATABASE', 'myapp'),
    'username' => $env->get('DB_USERNAME', 'root'),
    'password' => $env->get('DB_PASSWORD', ''),
    'charset' => $env->get('DB_CHARSET', 'utf8mb4'),
    'ssl' => $env->get('DB_SSL', false),
];

Feature Flags

php
$env = new Env(root_path());
$env->load();

// Check feature flags
$newFeatureEnabled = $env->get('FEATURE_NEW_UI', false);
$betaFeaturesEnabled = $env->get('ENABLE_BETA', false);
$apiV2Enabled = $env->get('API_V2_ENABLED', false);

if ($newFeatureEnabled) {
    // Load new UI components
}

if ($betaFeaturesEnabled) {
    // Enable beta features
}

Multi-Environment Loading

php
// Load base .env and environment-specific overrides
$appEnv = getenv('APP_ENV') ?: 'local';

$env = new Env(root_path(), ['.env', ".env.{$appEnv}"]);
$env->load();

// Environment-specific values override base values
$debug = $env->get('APP_DEBUG', false);

Conditional Configuration

php
$env = new Env(root_path());
$env->load();

// Check if optional services are configured
if ($env->has('REDIS_HOST')) {
    $redisConfig = [
        'host' => $env->get('REDIS_HOST'),
        'port' => $env->get('REDIS_PORT', 6379),
        'password' => $env->get('REDIS_PASSWORD', null),
        'database' => $env->get('REDIS_DATABASE', 0),
    ];
    // Initialize Redis connection
}

if ($env->has('MAIL_HOST')) {
    $mailConfig = [
        'host' => $env->get('MAIL_HOST'),
        'port' => $env->get('MAIL_PORT', 587),
        'username' => $env->get('MAIL_USERNAME'),
        'password' => $env->get('MAIL_PASSWORD'),
        'encryption' => $env->get('MAIL_ENCRYPTION', 'tls'),
    ];
    // Initialize mail service
}

Validation and Error Handling

php
$env = new Env(root_path());
$env->load();

// Validate critical configuration
try {
    $env->requireNotEmpty(['APP_KEY', 'APP_ENV']);
    $env->requireOneOf('APP_ENV', ['local', 'staging', 'production']);
    
    if ($env->get('APP_ENV') === 'production') {
        $env->requireNotEmpty(['DATABASE_PASSWORD', 'APP_KEY']);
    }
} catch (Exception $e) {
    report()->critical('Environment configuration error', [
        'error' => $e->getMessage()
    ]);
    die('Configuration error. Please check your .env file.');
}

10.6 Env Helper Function

The env() global helper function provides convenient access to environment variables with automatic type casting. It's a wrapper around the Env class that maintains a singleton instance.

Basic Usage

Get Environment Variable

php
// Get variable with automatic type casting
$appName = env('APP_NAME');
// Returns string value

// Get variable with default value
$debug = env('APP_DEBUG', false);
// Returns false if APP_DEBUG not set

Usage With and Without Parameters

Without Parameters

When called without parameters, env() returns the Env instance:

php
// Get Env instance
$envInstance = env();

// Use Env methods
$envInstance->load();
$envInstance->require(['APP_NAME', 'APP_ENV']);
$allVars = $envInstance->all();

With Parameters

When called with parameters, env() gets a specific variable:

php
// Get single variable
$name = env('APP_NAME');

// Get with default value
$port = env('APP_PORT', 8000);

Default Value Parameter

The default value parameter serves two purposes:

  1. Fallback Value: Returned if the environment variable doesn't exist
  2. Type Hint: Determines the return type through automatic casting

String Default

php
$name = env('APP_NAME', 'ElliePHP');
// Returns string, defaults to 'ElliePHP'

$host = env('DATABASE_HOST', 'localhost');
// Returns string, defaults to 'localhost'

Boolean Default

php
$debug = env('APP_DEBUG', false);
// Returns boolean, defaults to false

$maintenance = env('MAINTENANCE_MODE', true);
// Returns boolean, defaults to true

Integer Default

php
$port = env('APP_PORT', 8000);
// Returns integer, defaults to 8000

$maxConnections = env('MAX_CONNECTIONS', 100);
// Returns integer, defaults to 100

Float Default

php
$timeout = env('TIMEOUT', 30.0);
// Returns float, defaults to 30.0

$taxRate = env('TAX_RATE', 0.15);
// Returns float, defaults to 0.15

Null Default

php
$optional = env('OPTIONAL_VALUE', null);
// Returns value as-is or null (no type casting)

Practical Examples

Application Configuration

php
// Basic app configuration
$config = [
    'name' => env('APP_NAME', 'ElliePHP'),
    'env' => env('APP_ENV', 'production'),
    'debug' => env('APP_DEBUG', false),
    'url' => env('APP_URL', 'http://localhost'),
    'timezone' => env('APP_TIMEZONE', 'UTC'),
];

Database Configuration

php
$database = [
    'host' => env('DB_HOST', 'localhost'),
    'port' => env('DB_PORT', 3306),
    'database' => env('DB_DATABASE', 'myapp'),
    'username' => env('DB_USERNAME', 'root'),
    'password' => env('DB_PASSWORD', ''),
    'charset' => env('DB_CHARSET', 'utf8mb4'),
    'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
    'prefix' => env('DB_PREFIX', ''),
    'strict' => env('DB_STRICT', true),
];

Cache Configuration

php
$cache = [
    'driver' => env('CACHE_DRIVER', 'file'),
    'prefix' => env('CACHE_PREFIX', 'ellie_cache'),
    
    // Redis configuration
    'redis' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'port' => env('REDIS_PORT', 6379),
        'password' => env('REDIS_PASSWORD', null),
        'database' => env('REDIS_DATABASE', 0),
        'timeout' => env('REDIS_TIMEOUT', 5.0),
    ],
];

Mail Configuration

php
$mail = [
    'driver' => env('MAIL_DRIVER', 'smtp'),
    'host' => env('MAIL_HOST', 'smtp.mailtrap.io'),
    'port' => env('MAIL_PORT', 2525),
    'username' => env('MAIL_USERNAME'),
    'password' => env('MAIL_PASSWORD'),
    'encryption' => env('MAIL_ENCRYPTION', 'tls'),
    'from' => [
        'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
        'name' => env('MAIL_FROM_NAME', 'ElliePHP'),
    ],
];

Feature Flags

php
// Check if features are enabled
if (env('FEATURE_NEW_UI', false)) {
    // Load new UI
}

if (env('ENABLE_API_V2', false)) {
    // Enable API v2 routes
}

if (env('MAINTENANCE_MODE', false)) {
    // Show maintenance page
    return response()->json([
        'message' => 'Service temporarily unavailable'
    ], 503);
}

Service Configuration

php
// AWS Configuration
$aws = [
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
    'bucket' => env('AWS_BUCKET'),
];

// Stripe Configuration
$stripe = [
    'key' => env('STRIPE_KEY'),
    'secret' => env('STRIPE_SECRET'),
    'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
];

// Pusher Configuration
$pusher = [
    'app_id' => env('PUSHER_APP_ID'),
    'key' => env('PUSHER_APP_KEY'),
    'secret' => env('PUSHER_APP_SECRET'),
    'cluster' => env('PUSHER_APP_CLUSTER', 'mt1'),
    'encrypted' => env('PUSHER_ENCRYPTED', true),
];

Logging Configuration

php
$logging = [
    'level' => env('LOG_LEVEL', 'debug'),
    'channel' => env('LOG_CHANNEL', 'stack'),
    'path' => storage_logs_path(env('LOG_FILE', 'app.log')),
    'max_files' => env('LOG_MAX_FILES', 7),
];

Session Configuration

php
$session = [
    'driver' => env('SESSION_DRIVER', 'file'),
    'lifetime' => env('SESSION_LIFETIME', 120),
    'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
    'encrypt' => env('SESSION_ENCRYPT', false),
    'path' => env('SESSION_PATH', '/'),
    'domain' => env('SESSION_DOMAIN', null),
    'secure' => env('SESSION_SECURE_COOKIE', false),
    'http_only' => env('SESSION_HTTP_ONLY', true),
    'same_site' => env('SESSION_SAME_SITE', 'lax'),
];

API Configuration

php
$api = [
    'rate_limit' => env('API_RATE_LIMIT', 60),
    'rate_limit_window' => env('API_RATE_LIMIT_WINDOW', 60),
    'timeout' => env('API_TIMEOUT', 30.0),
    'retry_attempts' => env('API_RETRY_ATTEMPTS', 3),
    'verify_ssl' => env('API_VERIFY_SSL', true),
];

Environment-Specific Behavior

php
$appEnv = env('APP_ENV', 'production');

// Development-specific settings
if ($appEnv === 'local') {
    ini_set('display_errors', '1');
    error_reporting(E_ALL);
}

// Production-specific settings
if ($appEnv === 'production') {
    ini_set('display_errors', '0');
    
    // Require production variables
    if (!env('APP_KEY')) {
        die('APP_KEY must be set in production');
    }
}

// Staging-specific settings
if ($appEnv === 'staging') {
    // Enable debug logging
    $debugLevel = env('DEBUG_LEVEL', 'info');
}

Conditional Service Initialization

php
// Initialize Redis only if configured
if (env('REDIS_HOST')) {
    $redis = new Redis();
    $redis->connect(
        env('REDIS_HOST'),
        env('REDIS_PORT', 6379)
    );
    
    if (env('REDIS_PASSWORD')) {
        $redis->auth(env('REDIS_PASSWORD'));
    }
}

// Initialize queue only if enabled
if (env('QUEUE_ENABLED', false)) {
    $queue = new QueueManager([
        'driver' => env('QUEUE_DRIVER', 'sync'),
        'connection' => env('QUEUE_CONNECTION', 'default'),
    ]);
}

Type-Safe Configuration Access

php
// Boolean values
$debug = env('APP_DEBUG', false);           // boolean
$maintenance = env('MAINTENANCE_MODE', false); // boolean

// Integer values
$port = env('APP_PORT', 8000);              // integer
$maxConnections = env('MAX_CONNECTIONS', 100); // integer

// Float values
$timeout = env('TIMEOUT', 30.0);            // float
$taxRate = env('TAX_RATE', 0.15);           // float

// String values
$name = env('APP_NAME', 'ElliePHP');        // string
$timezone = env('APP_TIMEZONE', 'UTC');     // string

// Null values (no casting)
$optional = env('OPTIONAL_VALUE', null);    // mixed

Using Env Instance Methods

php
// Get Env instance for advanced operations
$env = env();

// Check if variable exists
if ($env->has('REDIS_HOST')) {
    // Redis is configured
}

// Get all variables
$allVars = $env->all();

// Require specific variables
try {
    $env->require(['APP_NAME', 'APP_KEY', 'APP_ENV']);
} catch (Exception $e) {
    die('Missing required environment variables');
}

// Require non-empty variables
$env->requireNotEmpty(['DATABASE_HOST', 'DATABASE_NAME']);

// Require specific values
$env->requireOneOf('APP_ENV', ['local', 'staging', 'production']);

Best Practices

php
// Always provide sensible defaults
$timeout = env('API_TIMEOUT', 30.0);

// Use type-appropriate defaults
$debug = env('APP_DEBUG', false);        // boolean default
$port = env('APP_PORT', 8000);           // integer default
$rate = env('TAX_RATE', 0.15);           // float default

// Check for optional configuration
if (env('FEATURE_ENABLED', false)) {
    // Feature is explicitly enabled
}

// Use null for truly optional values
$optional = env('OPTIONAL_SERVICE_URL', null);
if ($optional !== null) {
    // Service is configured
}

// Validate critical configuration early
if (env('APP_ENV') === 'production' && !env('APP_KEY')) {
    throw new RuntimeException('APP_KEY must be set in production');
}

10.7 Path Helper Functions

ElliePHP provides a set of global helper functions for accessing common directory paths. These functions ensure consistent path handling across your application and make it easy to reference files and directories.

root_path()

Get the root path of the application:

php
// Get root directory
$root = root_path();
// '/var/www/myapp'

// Get path to file in root
$envFile = root_path('.env');
// '/var/www/myapp/.env'

// Get path to subdirectory
$configDir = root_path('configs');
// '/var/www/myapp/configs'

// Get path to nested file
$composerJson = root_path('composer.json');
// '/var/www/myapp/composer.json'

Usage Examples:

php
// Load environment file
$env = new Env(root_path());
$env->load();

// Read composer.json
$composer = File::json(root_path('composer.json'));

// Check if .env exists
if (File::exists(root_path('.env'))) {
    // Load environment
}

app_path()

Get the application path (app/ directory):

php
// Get app directory
$appDir = app_path();
// '/var/www/myapp/app'

// Get path to controller
$controller = app_path('Http/Controllers/UserController.php');
// '/var/www/myapp/app/Http/Controllers/UserController.php'

// Get path to service
$service = app_path('Services/UserService.php');
// '/var/www/myapp/app/Services/UserService.php'

// Get path to middleware
$middleware = app_path('Http/Middleware/AuthMiddleware.php');
// '/var/www/myapp/app/Http/Middleware/AuthMiddleware.php'

Usage Examples:

php
// Load all controllers
$controllers = File::files(app_path('Http/Controllers'));

// Check if service exists
if (File::exists(app_path('Services/EmailService.php'))) {
    // Service is available
}

// Read template file
$template = File::get(app_path('templates/email.html'));

routes_path()

Get the routes path (routes/ directory):

php
// Get routes directory
$routesDir = routes_path();
// '/var/www/myapp/routes'

// Get path to router file
$router = routes_path('router.php');
// '/var/www/myapp/routes/router.php'

// Get path to API routes
$apiRoutes = routes_path('api.php');
// '/var/www/myapp/routes/api.php'

// Get path to web routes
$webRoutes = routes_path('web.php');
// '/var/www/myapp/routes/web.php'

Usage Examples:

php
// Load route files
require routes_path('router.php');
require routes_path('api.php');

// Check if route file exists
if (File::exists(routes_path('admin.php'))) {
    require routes_path('admin.php');
}

// Get all route files
$routeFiles = File::files(routes_path());

storage_path()

Get the storage path (storage/ directory):

php
// Get storage directory
$storageDir = storage_path();
// '/var/www/myapp/storage'

// Get path to uploads
$uploads = storage_path('uploads');
// '/var/www/myapp/storage/uploads'

// Get path to specific file
$file = storage_path('uploads/document.pdf');
// '/var/www/myapp/storage/uploads/document.pdf'

// Get path to temp directory
$temp = storage_path('temp');
// '/var/www/myapp/storage/temp'

Usage Examples:

php
// Save uploaded file
$destination = storage_path('uploads/' . $filename);
File::move($uploadedFile, $destination);

// Create backup directory
File::makeDirectory(storage_path('backups'));

// Clean temp directory
File::cleanDirectory(storage_path('temp'));

// Get all uploaded files
$files = File::files(storage_path('uploads'));

storage_logs_path()

Get the storage logs path (storage/Logs/ directory):

php
// Get logs directory
$logsDir = storage_logs_path();
// '/var/www/myapp/storage/Logs'

// Get path to app log
$appLog = storage_logs_path('app.log');
// '/var/www/myapp/storage/Logs/app.log'

// Get path to exception log
$exceptionLog = storage_logs_path('exceptions.log');
// '/var/www/myapp/storage/Logs/exceptions.log'

// Get path to custom log
$customLog = storage_logs_path('custom.log');
// '/var/www/myapp/storage/Logs/custom.log'

Usage Examples:

php
// Configure logger
$logger = new Logger('app');
$logger->pushHandler(
    new StreamHandler(storage_logs_path('app.log'), LogLevel::DEBUG)
);

// Read log file
$logContent = File::get(storage_logs_path('app.log'));

// Append to log
File::append(storage_logs_path('custom.log'), $logMessage);

// Clean old logs
$logFiles = File::files(storage_logs_path());
foreach ($logFiles as $file) {
    if (File::isOlderThan($file, 30 * 86400)) {
        File::delete($file);
    }
}

storage_cache_path()

Get the storage cache path (storage/Cache/ directory):

php
// Get cache directory
$cacheDir = storage_cache_path();
// '/var/www/myapp/storage/Cache'

// Get path to cache file
$cacheFile = storage_cache_path('data.cache');
// '/var/www/myapp/storage/Cache/data.cache'

// Get path to compiled container
$container = storage_cache_path('CompiledContainer.php');
// '/var/www/myapp/storage/Cache/CompiledContainer.php'

// Get path to route cache
$routeCache = storage_cache_path('routes.cache');
// '/var/www/myapp/storage/Cache/routes.cache'

Usage Examples:

php
// Configure file cache driver
$cache = CacheFactory::createFileDriver([
    'path' => storage_cache_path()
]);

// Configure SQLite cache driver
$cache = CacheFactory::createSQLiteDriver([
    'path' => storage_cache_path('cache.db')
]);

// Save cache file
File::put(storage_cache_path('config.cache'), serialize($config));

// Load cache file
if (File::exists(storage_cache_path('config.cache'))) {
    $config = unserialize(File::get(storage_cache_path('config.cache')));
}

// Clear cache directory
File::cleanDirectory(storage_cache_path());

Practical Examples

File Upload Handling

php
// Handle file upload
$uploadedFile = $_FILES['document']['tmp_name'];
$filename = uniqid() . '_' . $_FILES['document']['name'];
$destination = storage_path('uploads/' . $filename);

// Ensure upload directory exists
File::makeDirectory(storage_path('uploads'));

// Move uploaded file
File::move($uploadedFile, $destination);

// Log upload
report()->info('File uploaded', [
    'filename' => $filename,
    'path' => $destination,
    'size' => File::humanSize($destination)
]);

Configuration Loading

php
// Load configuration files
$appConfig = require root_path('configs/app.php');
$dbConfig = require root_path('configs/database.php');

// Load JSON configuration
$settings = File::json(root_path('configs/settings.json'));

// Load environment
$env = new Env(root_path());
$env->load();

Log Management

php
// Configure application logger
$appLogger = new Logger('app');
$appLogger->pushHandler(
    new StreamHandler(storage_logs_path('app.log'), LogLevel::DEBUG)
);

// Configure exception logger
$exceptionLogger = new Logger('exceptions');
$exceptionLogger->pushHandler(
    new StreamHandler(storage_logs_path('exceptions.log'), LogLevel::ERROR)
);

// Create custom log file
$customLog = storage_logs_path('api-' . date('Y-m-d') . '.log');
File::append($customLog, "[" . date('H:i:s') . "] API request received\n");

Cache Management

php
// File cache configuration
$fileCache = CacheFactory::createFileDriver([
    'path' => storage_cache_path(),
    'create_directory' => true,
    'directory_permissions' => 0755
]);

// SQLite cache configuration
$sqliteCache = CacheFactory::createSQLiteDriver([
    'path' => storage_cache_path('cache.db')
]);

// Custom cache file
$cacheKey = 'user_data_' . $userId;
$cacheFile = storage_cache_path($cacheKey . '.cache');

if (File::exists($cacheFile) && !File::isOlderThan($cacheFile, 3600)) {
    $data = unserialize(File::get($cacheFile));
} else {
    $data = fetchUserData($userId);
    File::put($cacheFile, serialize($data));
}

Backup Operations

php
// Create backup directory
$backupDir = storage_path('backups/' . date('Y-m-d'));
File::makeDirectory($backupDir);

// Backup database
$dbFile = storage_path('database.sqlite');
$backupFile = $backupDir . '/database.sqlite';
File::copy($dbFile, $backupFile);

// Backup logs
$logsBackup = $backupDir . '/logs';
File::copyDirectory(storage_logs_path(), $logsBackup);

// Log backup
report()->info('Backup created', [
    'directory' => $backupDir,
    'size' => File::humanSize($backupFile)
]);

Template Processing

php
// Load email template
$template = File::get(app_path('templates/welcome-email.html'));

// Process template
$template = str_replace('{name}', $userName, $template);
$template = str_replace('{url}', $verificationUrl, $template);

// Save processed template
$processedFile = storage_path('emails/' . $userId . '.html');
File::makeDirectory(storage_path('emails'));
File::put($processedFile, $template);

Route Loading

php
// Load main routes
require routes_path('router.php');

// Load API routes if they exist
if (File::exists(routes_path('api.php'))) {
    require routes_path('api.php');
}

// Load admin routes in production
if (env('APP_ENV') === 'production' && File::exists(routes_path('admin.php'))) {
    require routes_path('admin.php');
}

// Load all route files dynamically
$routeFiles = File::files(routes_path());
foreach ($routeFiles as $file) {
    if (File::extension($file) === 'php') {
        require $file;
    }
}

Service Discovery

php
// Get all services
$services = File::files(app_path('Services'));

// Load services dynamically
foreach ($services as $service) {
    $className = File::name($service);
    $fullClass = "ElliePHP\\Framework\\Application\\Services\\{$className}";
    
    if (class_exists($fullClass)) {
        // Register service in container
        container()->set($fullClass, new $fullClass());
    }
}

Middleware Loading

php
// Get all middleware files
$middlewareFiles = File::files(app_path('Http/Middleware'));

// Register middleware
foreach ($middlewareFiles as $file) {
    $className = File::name($file);
    $fullClass = "ElliePHP\\Framework\\Application\\Http\\Middleware\\{$className}";
    
    if (class_exists($fullClass)) {
        // Add to middleware stack
    }
}

Development vs Production Paths

php
// Use different paths based on environment
$env = env('APP_ENV', 'production');

if ($env === 'local') {
    // Development: use local storage
    $uploadPath = storage_path('uploads');
} else {
    // Production: might use different storage
    $uploadPath = env('UPLOAD_PATH', storage_path('uploads'));
}

// Ensure directory exists
File::makeDirectory($uploadPath);

Path Validation

php
// Validate paths exist
$requiredPaths = [
    storage_path(),
    storage_logs_path(),
    storage_cache_path(),
    app_path(),
    routes_path()
];

foreach ($requiredPaths as $path) {
    if (!File::isDirectory($path)) {
        File::makeDirectory($path, 0755, true);
        report()->info('Created directory', ['path' => $path]);
    }
}

Best Practices

php
// Always use path helpers instead of hardcoded paths
// Good:
$file = storage_path('uploads/document.pdf');

// Bad:
$file = '/var/www/myapp/storage/uploads/document.pdf';

// Combine with File utility methods
File::makeDirectory(storage_path('uploads'));
File::put(storage_path('data.json'), $json);

// Use for configuration
$config = [
    'upload_path' => storage_path('uploads'),
    'log_path' => storage_logs_path(),
    'cache_path' => storage_cache_path(),
];

// Check existence before operations
if (File::exists(storage_path('uploads'))) {
    $files = File::files(storage_path('uploads'));
}


11. Configuration

11.1 Configuration File Structure

ElliePHP uses a simple, file-based configuration system where configuration files are stored in the configs/ directory. Each configuration file is a PHP file that returns an array of configuration values.

Directory Organization

The configs/ directory contains all framework and application configuration files:

configs/
├── Commands.php      # Console command registration
├── Container.php     # Dependency injection bindings
├── Env.php          # Required environment variables
└── Middleware.php   # Global middleware configuration

Each configuration file serves a specific purpose and is automatically loaded by the framework when needed.

Configuration File Format

All configuration files follow the same format: they are PHP files that return an associative array:

php
<?php

/**
 * Example Configuration File
 *
 * Brief description of what this configuration controls.
 */

return [
    'key1' => 'value1',
    'key2' => 'value2',
    
    'nested' => [
        'option1' => true,
        'option2' => 'value',
    ],
];

This format provides several benefits:

  • Type Safety: PHP's type system ensures configuration values are properly typed
  • IDE Support: Full autocomplete and refactoring support
  • Dynamic Values: Can use PHP expressions, environment variables, and functions
  • Comments: Can include documentation directly in configuration files

Example Configuration Files

Commands.php - Console command registration:

php
<?php

use ElliePHP\Framework\Application\Console\Command\CacheClearCommand;
use ElliePHP\Framework\Application\Console\Command\MakeControllerCommand;
use ElliePHP\Framework\Application\Console\Command\RoutesCommand;
use ElliePHP\Framework\Application\Console\Command\ServeCommand;

return [
    'app' => [
        MakeControllerCommand::class,
        CacheClearCommand::class,
        ServeCommand::class,
        RoutesCommand::class
    ]
];

Container.php - Dependency injection service bindings:

php
<?php

use Psr\Container\ContainerInterface;

return [
    // Bind interfaces to implementations
    UserRepositoryInterface::class => DI\autowire(UserRepository::class),
    
    // Factory definitions
    'database' => function (ContainerInterface $c) {
        return new PDO(
            env('DB_DSN'),
            env('DB_USER'),
            env('DB_PASS')
        );
    },
    
    // Singleton services with lazy loading
    CacheService::class => DI\create(CacheService::class)->lazy(),
];

Env.php - Required environment variable definitions:

php
<?php

return [
    'required_configs' => [
        'APP_NAME',
        'APP_TIMEZONE',
        'APP_ENV',
    ],
];

Middleware.php - Global middleware stack:

php
<?php

return [
    'global_middlewares' => [
        \ElliePHP\Framework\Application\Http\Middlewares\LoggingMiddleware::class,
        \ElliePHP\Framework\Application\Http\Middlewares\CorsMiddleware::class,
    ],
];

Configuration Loading

Configuration files are loaded automatically by the framework:

  • Automatic Loading: The config() helper loads all configuration files on first use
  • Lazy Loading: Individual files are loaded only when their values are accessed
  • Caching: In production, configuration can be cached for better performance
  • Merge Strategy: Multiple configuration files are merged into a single configuration array

The framework uses the ConfigParser class to manage configuration loading and access, providing a consistent interface for all configuration operations.

11.2 Environment Variables

ElliePHP uses environment variables to manage configuration that varies between environments (development, staging, production). Environment variables are stored in a .env file in the project root and accessed using the env() helper function.

.env File Format

The .env file uses a simple KEY=VALUE format:

env
# Application Configuration
APP_NAME='ElliePHP'
APP_DEBUG=true
APP_TIMEZONE='UTC'

# Cache Configuration
CACHE_DRIVER=file

# Redis Configuration (when using redis cache driver)
REDIS_HOST='127.0.0.1'
REDIS_PORT=6379
REDIS_PASSWORD=null
REDIS_DATABASE=0
REDIS_TIMEOUT=5

Available Environment Variables

Here are all the environment variables available in ElliePHP, based on .env.example:

Application Settings

VariableTypeDefaultDescription
APP_NAMEstring'ElliePHP'Application name used in logs and error messages
APP_DEBUGbooleantrueEnable debug mode for detailed error messages
APP_TIMEZONEstring'UTC'Default timezone for date/time operations
APP_ENVstring-Application environment (development, production)

Cache Configuration

VariableTypeDefaultDescription
CACHE_DRIVERstring'file'Cache driver to use (file, redis, sqlite, apcu)

Redis Configuration

These variables are used when CACHE_DRIVER=redis:

VariableTypeDefaultDescription
REDIS_HOSTstring'127.0.0.1'Redis server hostname or IP address
REDIS_PORTinteger6379Redis server port
REDIS_PASSWORDstringnullRedis authentication password (null for no auth)
REDIS_DATABASEinteger0Redis database number (0-15)
REDIS_TIMEOUTinteger5Connection timeout in seconds

Required vs Optional Variables

The framework distinguishes between required and optional environment variables. Required variables must be set or the application will fail to start.

Required Variables

Required variables are defined in configs/Env.php:

php
<?php

return [
    'required_configs' => [
        'APP_NAME',
        'APP_TIMEZONE',
        'APP_ENV',
    ],
];

If any required variable is missing, the framework will throw an exception during bootstrap with a clear error message indicating which variables are missing.

Optional Variables

All other variables are optional and will use their default values if not set:

php
// Optional with default value
$cacheDriver = env('CACHE_DRIVER', 'file');

// Optional, returns null if not set
$redisPassword = env('REDIS_PASSWORD');

Special Values

The .env file supports special values that are automatically converted:

  • true / false - Converted to boolean values
  • null - Converted to PHP null
  • empty - Converted to empty string
  • Quoted strings - Quotes are removed: 'value' becomes value
  • Numbers - Automatically cast to integers or floats
env
# Boolean values
APP_DEBUG=true
FEATURE_ENABLED=false

# Null value
REDIS_PASSWORD=null

# Empty string
OPTIONAL_VALUE=empty

# Quoted strings
APP_NAME='My Application'

# Numbers
REDIS_PORT=6379
TIMEOUT=5.5

Environment Variable Type Casting

The env() function automatically casts values based on the default value type:

php
// Returns boolean
$debug = env('APP_DEBUG', false);

// Returns integer
$port = env('REDIS_PORT', 6379);

// Returns float
$timeout = env('TIMEOUT', 5.0);

// Returns string
$name = env('APP_NAME', 'ElliePHP');

See Section 10.5 Environment Utilities for more details on the Env class and type casting behavior.

Adding Custom Environment Variables

To add custom environment variables:

  1. Add the variable to your .env file:
env
# Custom Configuration
MY_API_KEY='your-api-key-here'
MY_API_URL='https://api.example.com'
  1. If the variable is required, add it to configs/Env.php:
php
return [
    'required_configs' => [
        'APP_NAME',
        'APP_TIMEZONE',
        'APP_ENV',
        'MY_API_KEY',  // Add your required variable
    ],
];
  1. Access the variable using the env() helper:
php
$apiKey = env('MY_API_KEY');
$apiUrl = env('MY_API_URL', 'https://default-api.com');

Best Practices

  • Never commit .env files: Add .env to .gitignore to prevent sensitive data from being committed
  • Use .env.example: Maintain a .env.example file with all variables (using placeholder values) as documentation
  • Document required variables: Always update configs/Env.php when adding required variables
  • Use descriptive names: Use clear, uppercase names with underscores (e.g., DATABASE_HOST)
  • Group related variables: Use prefixes to group related variables (e.g., REDIS_*, DB_*)
  • Provide defaults: Always provide sensible defaults for optional variables

11.3 Configuration Access

ElliePHP provides a convenient config() helper function for accessing and setting configuration values. The function supports dot notation for accessing nested configuration values and provides a clean, consistent API.

Getting Configuration Values

Use the config() function with a dot-notation key to retrieve configuration values:

php
// Get a top-level configuration value
$commands = config('Commands.app');

// Get a nested configuration value using dot notation
$middlewares = config('Middleware.global_middlewares');

// Get with a default value if the key doesn't exist
$customValue = config('app.custom_setting', 'default-value');

Dot Notation Access

Dot notation allows you to access deeply nested configuration values:

php
// Configuration file: configs/database.php
return [
    'connections' => [
        'mysql' => [
            'host' => 'localhost',
            'port' => 3306,
            'database' => 'myapp',
        ],
    ],
];

// Access nested values
$host = config('database.connections.mysql.host');     // 'localhost'
$port = config('database.connections.mysql.port');     // 3306
$database = config('database.connections.mysql.database'); // 'myapp'

The dot notation format is: {filename}.{key1}.{key2}.{keyN}

  • First segment is the configuration file name (without .php)
  • Subsequent segments navigate through nested arrays
  • Returns null if any segment doesn't exist (unless a default is provided)

Setting Configuration Values

You can set configuration values at runtime using the config() function:

php
// Set a single value
config(['app.name' => 'My Application']);

// Set multiple values
config([
    'app.name' => 'My Application',
    'app.version' => '1.0.0',
    'app.debug' => false,
]);

// Set nested values
config(['database.connections.mysql.host' => '192.168.1.100']);

Important Notes:

  • Setting configuration values only affects the current request
  • Changes are not persisted to configuration files
  • Useful for testing or runtime configuration overrides
  • For permanent changes, edit the configuration files directly

Getting the ConfigParser Instance

Call config() without arguments to get the ConfigParser instance:

php
$configParser = config();

// Use ConfigParser methods directly
$allConfig = $configParser->all();
$hasKey = $configParser->has('Middleware.global_middlewares');

Practical Examples

Example 1: Accessing Command Configuration

php
// Get all registered commands
$commands = config('Commands.app');

// Iterate through commands
foreach ($commands as $commandClass) {
    echo "Registered command: {$commandClass}\n";
}

Example 2: Accessing Middleware Configuration

php
// Get global middleware stack
$middlewares = config('Middleware.global_middlewares');

// Check if a specific middleware is registered
if (in_array(CorsMiddleware::class, $middlewares)) {
    echo "CORS middleware is enabled\n";
}

Example 3: Dynamic Configuration in Services

php
class ApiService
{
    private string $apiUrl;
    private string $apiKey;
    
    public function __construct()
    {
        // Get configuration with defaults
        $this->apiUrl = config('api.url', 'https://api.example.com');
        $this->apiKey = config('api.key', '');
        
        if (empty($this->apiKey)) {
            throw new RuntimeException('API key not configured');
        }
    }
}

Example 4: Runtime Configuration Override

php
// Temporarily override configuration for testing
config(['app.debug' => true]);

// Perform operations with debug enabled
$result = someOperation();

// Configuration change only affects current request

Example 5: Checking Configuration Existence

php
// Check if a configuration key exists
if (config()->has('database.connections.mysql')) {
    $connection = config('database.connections.mysql');
    // Use the connection configuration
} else {
    // Use default connection
    $connection = config('database.connections.default');
}

Configuration vs Environment Variables

Understanding when to use configuration files vs environment variables:

Use Configuration Files For:

  • Application structure (commands, middleware, routes)
  • Service bindings and dependencies
  • Default values and fallbacks
  • Complex nested structures
  • Values that don't change between environments

Use Environment Variables For:

  • Credentials and secrets (API keys, passwords)
  • Environment-specific values (database hosts, cache drivers)
  • Feature flags that vary by environment
  • Values that should never be committed to version control

Combining Both:

Configuration files can reference environment variables:

php
// configs/database.php
return [
    'connections' => [
        'mysql' => [
            'host' => env('DB_HOST', 'localhost'),
            'port' => env('DB_PORT', 3306),
            'database' => env('DB_DATABASE', 'myapp'),
            'username' => env('DB_USERNAME', 'root'),
            'password' => env('DB_PASSWORD', ''),
        ],
    ],
];

// Access in your code
$dbConfig = config('database.connections.mysql');

This approach provides the best of both worlds: structured configuration with environment-specific values.

Performance Considerations

  • Lazy Loading: Configuration files are only loaded when accessed
  • Caching: All loaded configuration is cached in memory for the request duration
  • Production: Consider implementing configuration caching for production environments
  • Minimal Overhead: The ConfigParser is lightweight and optimized for performance

11.4 ConfigParser Class

The ConfigParser class is the core component of ElliePHP's configuration system. It provides methods for loading configuration files, accessing values with dot notation, and managing configuration data.

Class Overview

php
namespace ElliePHP\Framework\Support;

class ConfigParser
{
    public function __construct(string $configPath);
    public function load(string $name): void;
    public function loadAll(): void;
    public function get(string $key, mixed $default = null): mixed;
    public function set(string $key, mixed $value): void;
    public function has(string $key): bool;
    public function all(): array;
}

Creating a ConfigParser Instance

php
use ElliePHP\Framework\Support\ConfigParser;

// Create instance with configuration directory path
$config = new ConfigParser('/path/to/configs');

// The config() helper creates and manages a singleton instance
$config = config(); // Returns the global ConfigParser instance

load() Method

Load a single configuration file by name (without the .php extension):

php
// Load the Commands.php configuration file
$config->load('Commands');

// Load the Middleware.php configuration file
$config->load('Middleware');

// Throws RuntimeException if file doesn't exist
try {
    $config->load('NonExistent');
} catch (RuntimeException $e) {
    echo "Configuration file not found: " . $e->getMessage();
}

Method Signature:

php
public function load(string $name): void

Parameters:

  • $name - Configuration file name without .php extension

Throws:

  • RuntimeException - If the configuration file doesn't exist

loadAll() Method

Load all configuration files from the configuration directory:

php
// Load all .php files from the configs/ directory
$config->loadAll();

// After loading, all configuration is accessible
$commands = $config->get('Commands.app');
$middlewares = $config->get('Middleware.global_middlewares');

Method Signature:

php
public function loadAll(): void

This method scans the configuration directory for all .php files and loads them automatically. It's called by the config() helper on first use.

get() Method

Retrieve a configuration value using dot notation:

php
// Get a top-level value
$commands = $config->get('Commands.app');

// Get a nested value
$middlewares = $config->get('Middleware.global_middlewares');

// Get with a default value
$customValue = $config->get('app.custom_setting', 'default-value');

// Returns null if key doesn't exist and no default provided
$missing = $config->get('nonexistent.key'); // null

Method Signature:

php
public function get(string $key, mixed $default = null): mixed

Parameters:

  • $key - Dot notation key (e.g., 'file.section.key')
  • $default - Default value to return if key doesn't exist (optional)

Returns:

  • The configuration value, or $default if the key doesn't exist

Dot Notation Parsing:

The get() method parses dot notation keys to navigate nested arrays:

php
// Configuration: Commands.php
return [
    'app' => [
        'command1',
        'command2',
    ],
];

// Access with dot notation
$config->get('Commands.app');        // ['command1', 'command2']
$config->get('Commands.app.0');      // 'command1'
$config->get('Commands.missing');    // null
$config->get('Commands.missing', []); // []

set() Method

Set a configuration value using dot notation:

php
// Set a top-level value
$config->set('app.name', 'My Application');

// Set a nested value
$config->set('database.connections.mysql.host', 'localhost');

// Create nested structure if it doesn't exist
$config->set('new.nested.value', 'data');
// Creates: ['new' => ['nested' => ['value' => 'data']]]

Method Signature:

php
public function set(string $key, mixed $value): void

Parameters:

  • $key - Dot notation key (e.g., 'file.section.key')
  • $value - Value to set (any type)

Behavior:

  • Creates nested arrays automatically if they don't exist
  • Overwrites existing values
  • Changes are only in memory (not persisted to files)

has() Method

Check if a configuration key exists:

php
// Check if a key exists
if ($config->has('Middleware.global_middlewares')) {
    echo "Middleware configuration exists\n";
}

// Check nested keys
if ($config->has('database.connections.mysql.host')) {
    $host = $config->get('database.connections.mysql.host');
}

// Returns false for non-existent keys
$exists = $config->has('nonexistent.key'); // false

Method Signature:

php
public function has(string $key): bool

Parameters:

  • $key - Dot notation key to check

Returns:

  • true if the key exists, false otherwise

Note: This method checks for key existence, not value truthiness. A key with a null or false value will still return true.

all() Method

Get all loaded configuration data:

php
// Get all configuration as an associative array
$allConfig = $config->all();

// Structure:
// [
//     'Commands' => [...],
//     'Middleware' => [...],
//     'Container' => [...],
//     ...
// ]

// Iterate through all configuration
foreach ($allConfig as $file => $data) {
    echo "Configuration file: {$file}\n";
    print_r($data);
}

Method Signature:

php
public function all(): array

Returns:

  • Associative array with all loaded configuration data
  • Keys are configuration file names
  • Values are the arrays returned by each configuration file

Practical Examples

Example 1: Loading and Accessing Configuration

php
use ElliePHP\Framework\Support\ConfigParser;

// Create parser instance
$config = new ConfigParser(root_path('/configs'));

// Load specific configuration files
$config->load('Commands');
$config->load('Middleware');

// Access configuration values
$commands = $config->get('Commands.app');
$middlewares = $config->get('Middleware.global_middlewares');

Example 2: Dynamic Configuration Management

php
// Load all configuration
$config = config();

// Check if a feature is configured
if ($config->has('features.api_enabled')) {
    $apiEnabled = $config->get('features.api_enabled');
    
    if ($apiEnabled) {
        // Initialize API routes
    }
}

Example 3: Runtime Configuration Override

php
// Get the config instance
$config = config();

// Override configuration for testing
$config->set('app.debug', true);
$config->set('cache.driver', 'array');

// Run tests with overridden configuration
runTests();

// Configuration changes only affect current request

Example 4: Configuration Validation

php
// Validate required configuration keys
$requiredKeys = [
    'database.connections.mysql.host',
    'database.connections.mysql.database',
    'cache.driver',
];

$config = config();
$missing = [];

foreach ($requiredKeys as $key) {
    if (!$config->has($key)) {
        $missing[] = $key;
    }
}

if (!empty($missing)) {
    throw new RuntimeException(
        'Missing required configuration: ' . implode(', ', $missing)
    );
}

Example 5: Exporting Configuration

php
// Get all configuration for debugging
$config = config();
$allConfig = $config->all();

// Export to JSON for inspection
file_put_contents(
    'config-dump.json',
    json_encode($allConfig, JSON_PRETTY_PRINT)
);

Error Handling

The ConfigParser class throws exceptions for error conditions:

php
try {
    $config->load('NonExistentFile');
} catch (RuntimeException $e) {
    // Handle missing configuration file
    report()->error('Configuration file not found', [
        'file' => 'NonExistentFile',
        'error' => $e->getMessage(),
    ]);
}

Common Exceptions:

  • RuntimeException - Thrown when a configuration file doesn't exist

Best Practices

  1. Use the config() Helper: Instead of creating ConfigParser instances directly, use the config() helper which manages a singleton instance

  2. Load Configuration Early: Load all configuration during application bootstrap using loadAll()

  3. Check Existence Before Access: Use has() to check if optional configuration exists before accessing it

  4. Provide Defaults: Always provide sensible defaults when using get() for optional configuration

  5. Don't Persist Runtime Changes: Remember that set() only affects the current request; don't rely on it for persistent configuration changes

  6. Use Dot Notation Consistently: Stick to dot notation for all configuration access to maintain consistency

  7. Document Custom Configuration: Add comments to configuration files explaining the purpose and valid values for each key

11.5 Creating Custom Configuration Files

ElliePHP makes it easy to create custom configuration files for your application. Any PHP file in the configs/ directory that returns an array will be automatically loaded and accessible through the config() helper.

Configuration File Naming Conventions

Follow these naming conventions for configuration files:

  • PascalCase: Use PascalCase for file names (e.g., Database.php, ApiSettings.php)
  • Descriptive Names: Choose names that clearly describe the configuration purpose
  • Singular Form: Use singular names (e.g., Database.php not Databases.php)
  • No Prefixes: Don't use prefixes like config_ (the directory already indicates it's configuration)

Good Examples:

configs/
├── Database.php
├── Mail.php
├── Queue.php
├── ApiSettings.php
└── Features.php

Avoid:

configs/
├── config_database.php  ❌ (unnecessary prefix)
├── databases.php        ❌ (plural form)
├── db.php              ❌ (unclear abbreviation)
└── settings.php        ❌ (too generic)

Creating a Configuration File

Step 1: Create the File

Create a new PHP file in the configs/ directory:

php
<?php
// configs/Database.php

/**
 * Database Configuration
 *
 * Configure database connections and settings.
 */

return [
    'default' => env('DB_CONNECTION', 'mysql'),
    
    'connections' => [
        'mysql' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', 'localhost'),
            'port' => env('DB_PORT', 3306),
            'database' => env('DB_DATABASE', 'myapp'),
            'username' => env('DB_USERNAME', 'root'),
            'password' => env('DB_PASSWORD', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
        ],
        
        'sqlite' => [
            'driver' => 'sqlite',
            'database' => storage_path('database.sqlite'),
            'prefix' => '',
        ],
    ],
    
    'migrations' => [
        'table' => 'migrations',
        'path' => root_path('database/migrations'),
    ],
];

Step 2: Access the Configuration

The configuration is automatically available through the config() helper:

php
// Get the default connection
$defaultConnection = config('Database.default');

// Get MySQL connection settings
$mysqlConfig = config('Database.connections.mysql');

// Get a specific setting
$host = config('Database.connections.mysql.host');

// Get with a default value
$port = config('Database.connections.mysql.port', 3306);

Automatic Loading

Configuration files are loaded automatically in two ways:

1. Load All on First Use

When you first call config(), all configuration files are loaded:

php
// First call loads all configuration files
$config = config();

// All configuration is now available
$database = config('Database.connections.mysql');
$mail = config('Mail.smtp.host');

2. Lazy Loading

If a configuration file wasn't loaded, it's loaded automatically when accessed:

php
// Even if Database.php wasn't loaded yet, this works
$host = config('Database.connections.mysql.host');

// The framework automatically loads Database.php when needed

Configuration File Structure Best Practices

Use Nested Arrays for Organization

php
<?php
// configs/Mail.php

return [
    'default' => env('MAIL_MAILER', 'smtp'),
    
    'mailers' => [
        'smtp' => [
            'transport' => 'smtp',
            'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
            'port' => env('MAIL_PORT', 587),
            'encryption' => env('MAIL_ENCRYPTION', 'tls'),
            'username' => env('MAIL_USERNAME'),
            'password' => env('MAIL_PASSWORD'),
            'timeout' => null,
        ],
        
        'sendmail' => [
            'transport' => 'sendmail',
            'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs'),
        ],
    ],
    
    'from' => [
        'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
        'name' => env('MAIL_FROM_NAME', 'Example'),
    ],
];

Use Environment Variables for Sensitive Data

php
<?php
// configs/ApiSettings.php

return [
    'stripe' => [
        'key' => env('STRIPE_KEY'),
        'secret' => env('STRIPE_SECRET'),
        'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
    ],
    
    'aws' => [
        'key' => env('AWS_ACCESS_KEY_ID'),
        'secret' => env('AWS_SECRET_ACCESS_KEY'),
        'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
        'bucket' => env('AWS_BUCKET'),
    ],
];

Include Documentation Comments

php
<?php
// configs/Features.php

/**
 * Feature Flags Configuration
 *
 * Enable or disable application features.
 */

return [
    // API Features
    'api' => [
        'enabled' => env('API_ENABLED', true),
        'rate_limiting' => env('API_RATE_LIMITING', true),
        'versioning' => env('API_VERSIONING', true),
    ],
    
    // User Features
    'users' => [
        'registration' => env('USER_REGISTRATION', true),
        'email_verification' => env('USER_EMAIL_VERIFICATION', false),
        'two_factor_auth' => env('USER_2FA', false),
    ],
    
    // Experimental Features
    'experimental' => [
        'new_dashboard' => env('EXPERIMENTAL_DASHBOARD', false),
        'beta_features' => env('EXPERIMENTAL_BETA', false),
    ],
];

Practical Examples

Example 1: API Configuration

php
<?php
// configs/Api.php

return [
    'version' => 'v1',
    'prefix' => 'api',
    
    'rate_limiting' => [
        'enabled' => env('API_RATE_LIMIT_ENABLED', true),
        'max_attempts' => env('API_RATE_LIMIT_ATTEMPTS', 60),
        'decay_minutes' => env('API_RATE_LIMIT_DECAY', 1),
    ],
    
    'cors' => [
        'allowed_origins' => explode(',', env('API_CORS_ORIGINS', '*')),
        'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
        'allowed_headers' => ['Content-Type', 'Authorization'],
        'max_age' => 86400,
    ],
    
    'authentication' => [
        'driver' => env('API_AUTH_DRIVER', 'jwt'),
        'token_lifetime' => env('API_TOKEN_LIFETIME', 3600),
    ],
];

Usage:

php
// Check if rate limiting is enabled
if (config('Api.rate_limiting.enabled')) {
    $maxAttempts = config('Api.rate_limiting.max_attempts');
    // Apply rate limiting
}

// Get CORS configuration
$corsOrigins = config('Api.cors.allowed_origins');

Example 2: Queue Configuration

php
<?php
// configs/Queue.php

return [
    'default' => env('QUEUE_CONNECTION', 'sync'),
    
    'connections' => [
        'sync' => [
            'driver' => 'sync',
        ],
        
        'database' => [
            'driver' => 'database',
            'table' => 'jobs',
            'queue' => 'default',
            'retry_after' => 90,
        ],
        
        'redis' => [
            'driver' => 'redis',
            'connection' => 'default',
            'queue' => env('REDIS_QUEUE', 'default'),
            'retry_after' => 90,
            'block_for' => null,
        ],
    ],
    
    'failed' => [
        'driver' => env('QUEUE_FAILED_DRIVER', 'database'),
        'database' => 'mysql',
        'table' => 'failed_jobs',
    ],
];

Example 3: Application Settings

php
<?php
// configs/App.php

return [
    'name' => env('APP_NAME', 'ElliePHP'),
    'env' => env('APP_ENV', 'production'),
    'debug' => env('APP_DEBUG', false),
    'url' => env('APP_URL', 'http://localhost'),
    'timezone' => env('APP_TIMEZONE', 'UTC'),
    'locale' => env('APP_LOCALE', 'en'),
    
    'providers' => [
        // Service providers to load
        \App\Providers\AppServiceProvider::class,
        \App\Providers\RouteServiceProvider::class,
    ],
    
    'aliases' => [
        // Class aliases
        'Cache' => \ElliePHP\Components\Cache\Cache::class,
        'Request' => \ElliePHP\Components\Support\Http\Request::class,
        'Response' => \ElliePHP\Components\Support\Http\Response::class,
    ],
];

Using Configuration in Services

Configuration files work seamlessly with dependency injection:

php
class MailService
{
    private array $config;
    
    public function __construct()
    {
        // Load mail configuration
        $this->config = config('Mail.mailers.smtp');
    }
    
    public function send(string $to, string $subject, string $body): void
    {
        $host = $this->config['host'];
        $port = $this->config['port'];
        $username = $this->config['username'];
        $password = $this->config['password'];
        
        // Send email using configuration
    }
}

Configuration Validation

Create a helper to validate required configuration:

php
function validateConfig(array $requiredKeys): void
{
    $missing = [];
    
    foreach ($requiredKeys as $key) {
        if (!config()->has($key)) {
            $missing[] = $key;
        }
    }
    
    if (!empty($missing)) {
        throw new RuntimeException(
            'Missing required configuration keys: ' . implode(', ', $missing)
        );
    }
}

// Use in your application bootstrap
validateConfig([
    'Database.connections.mysql.host',
    'Database.connections.mysql.database',
    'Mail.mailers.smtp.host',
    'Api.authentication.driver',
]);

Tips for Custom Configuration

  1. Group Related Settings: Keep related configuration in the same file

  2. Use Environment Variables: Reference environment variables for values that change between environments

  3. Provide Defaults: Always provide sensible defaults using the second parameter of env()

  4. Document Options: Add comments explaining what each configuration option does

  5. Keep It Simple: Don't over-complicate configuration structure; flat is better than nested when possible

  6. Validate Early: Validate required configuration during application bootstrap

  7. Use Type Hints: When accessing configuration in classes, type hint the expected values

  8. Version Control: Commit configuration files to version control (but not .env files)

  9. Test Configuration: Write tests to ensure configuration is loaded correctly

  10. Avoid Logic: Configuration files should return data, not execute complex logic


12. Advanced Topics

12.1 Service Layer Pattern

The service layer pattern is a design pattern that encapsulates business logic in dedicated service classes, separating it from controllers and data access layers. This promotes code reusability, testability, and maintainability.

What is the Service Layer?

The service layer sits between your controllers and data access layer (repositories, models, or direct database access). It contains the business logic of your application - the rules, calculations, and workflows that define how your application behaves.

Benefits of the Service Layer Pattern:

  • Separation of Concerns: Controllers handle HTTP concerns, services handle business logic
  • Reusability: Business logic can be reused across multiple controllers or commands
  • Testability: Services can be tested independently without HTTP layer
  • Maintainability: Business logic is centralized and easier to modify
  • Dependency Management: Services can depend on other services, repositories, and utilities

Service Layer Architecture

Controller → Service → Repository → Database
    ↓          ↓           ↓
  HTTP     Business     Data
 Layer      Logic      Access

Controllers receive HTTP requests and delegate to services. Services perform business logic and coordinate with repositories. Repositories handle data persistence.

Creating a Service Class

Services are typically placed in the app/Services/ directory and use constructor injection for dependencies:

php
namespace ElliePHP\Framework\Application\Services;

use ElliePHP\Framework\Application\Repositories\UserRepository;
use Psr\SimpleCache\CacheInterface;

final readonly class UserService
{
    public function __construct(
        private UserRepository $repository,
        private CacheInterface $cache
    ) {
    }

    public function getAllUsers(): array
    {
        return $this->cache->remember('users', 3600, function () {
            return $this->repository->findAll();
        });
    }

    public function getUserById(int $id): ?array
    {
        $cacheKey = "user:{$id}";
        
        $user = $this->cache->get($cacheKey);
        if ($user !== null) {
            return $user;
        }
        
        $user = $this->repository->findById($id);
        if ($user !== null) {
            $this->cache->set($cacheKey, $user, 3600);
        }
        
        return $user;
    }

    public function createUser(array $data): array
    {
        // Validate data
        $this->validateUserData($data);
        
        // Create user
        $user = $this->repository->create($data);
        
        // Clear cache
        $this->cache->delete('users');
        
        // Log activity
        report()->info('User created', ['id' => $user['id']]);
        
        return $user;
    }

    public function updateUser(int $id, array $data): array
    {
        // Validate data
        $this->validateUserData($data);
        
        // Update user
        $user = $this->repository->update($id, $data);
        
        // Clear cache
        $this->cache->delete('users');
        $this->cache->delete("user:{$id}");
        
        // Log activity
        report()->info('User updated', ['id' => $id]);
        
        return $user;
    }

    public function deleteUser(int $id): bool
    {
        $result = $this->repository->delete($id);
        
        if ($result) {
            // Clear cache
            $this->cache->delete('users');
            $this->cache->delete("user:{$id}");
            
            // Log activity
            report()->info('User deleted', ['id' => $id]);
        }
        
        return $result;
    }

    private function validateUserData(array $data): void
    {
        if (empty($data['email']) || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException('Valid email is required');
        }
        
        if (empty($data['name']) || strlen($data['name']) < 2) {
            throw new \InvalidArgumentException('Name must be at least 2 characters');
        }
    }
}

Using Services in Controllers

Controllers should be thin - they receive requests, call services, and return responses:

php
namespace ElliePHP\Framework\Application\Http\Controllers;

use ElliePHP\Framework\Application\Services\UserService;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final readonly class UserController
{
    public function __construct(
        private UserService $userService
    ) {
    }

    public function index(): ResponseInterface
    {
        $users = $this->userService->getAllUsers();
        
        return response()->json(['users' => $users]);
    }

    public function show(int $id): ResponseInterface
    {
        $user = $this->userService->getUserById($id);
        
        if ($user === null) {
            return response()->notFound()->json([
                'error' => 'User not found'
            ]);
        }
        
        return response()->json(['user' => $user]);
    }

    public function store(ServerRequestInterface $request): ResponseInterface
    {
        try {
            $data = $request->getParsedBody();
            $user = $this->userService->createUser($data);
            
            return response()->created()->json([
                'message' => 'User created successfully',
                'user' => $user
            ]);
        } catch (\InvalidArgumentException $e) {
            return response()->badRequest()->json([
                'error' => $e->getMessage()
            ]);
        }
    }

    public function update(ServerRequestInterface $request, int $id): ResponseInterface
    {
        try {
            $data = $request->getParsedBody();
            $user = $this->userService->updateUser($id, $data);
            
            return response()->json([
                'message' => 'User updated successfully',
                'user' => $user
            ]);
        } catch (\InvalidArgumentException $e) {
            return response()->badRequest()->json([
                'error' => $e->getMessage()
            ]);
        }
    }

    public function destroy(int $id): ResponseInterface
    {
        $result = $this->userService->deleteUser($id);
        
        if (!$result) {
            return response()->notFound()->json([
                'error' => 'User not found'
            ]);
        }
        
        return response()->noContent();
    }
}

Service with Multiple Dependencies

Services can depend on multiple repositories, other services, and utilities:

php
namespace ElliePHP\Framework\Application\Services;

use ElliePHP\Framework\Application\Repositories\UserRepository;
use ElliePHP\Framework\Application\Repositories\OrderRepository;
use Psr\SimpleCache\CacheInterface;
use Psr\Log\LoggerInterface;

final readonly class OrderService
{
    public function __construct(
        private OrderRepository $orderRepository,
        private UserRepository $userRepository,
        private CacheInterface $cache,
        private LoggerInterface $logger,
        private EmailService $emailService
    ) {
    }

    public function createOrder(int $userId, array $items): array
    {
        // Verify user exists
        $user = $this->userRepository->findById($userId);
        if ($user === null) {
            throw new \RuntimeException('User not found');
        }
        
        // Calculate total
        $total = $this->calculateTotal($items);
        
        // Create order
        $order = $this->orderRepository->create([
            'user_id' => $userId,
            'items' => $items,
            'total' => $total,
            'status' => 'pending'
        ]);
        
        // Clear user's order cache
        $this->cache->delete("user:{$userId}:orders");
        
        // Send confirmation email
        $this->emailService->sendOrderConfirmation($user, $order);
        
        // Log order creation
        $this->logger->info('Order created', [
            'order_id' => $order['id'],
            'user_id' => $userId,
            'total' => $total
        ]);
        
        return $order;
    }

    private function calculateTotal(array $items): float
    {
        return array_reduce($items, function ($total, $item) {
            return $total + ($item['price'] * $item['quantity']);
        }, 0.0);
    }
}

Service Layer Best Practices

1. Keep Services Focused

Each service should have a single responsibility. Don't create a "God service" that does everything:

php
// Good: Focused services
UserService
OrderService
PaymentService
EmailService

// Bad: Monolithic service
ApplicationService // handles everything

2. Use Dependency Injection

Always inject dependencies through the constructor, never use global state or service locators:

php
// Good: Constructor injection
public function __construct(
    private UserRepository $repository,
    private CacheInterface $cache
) {}

// Bad: Service locator
public function getUsers(): array
{
    $repository = container(UserRepository::class); // Don't do this
}

3. Return Domain Objects or Arrays

Services should return data, not HTTP responses:

php
// Good: Return data
public function getUser(int $id): ?array
{
    return $this->repository->findById($id);
}

// Bad: Return HTTP response
public function getUser(int $id): ResponseInterface
{
    $user = $this->repository->findById($id);
    return response()->json($user); // This is controller's job
}

4. Handle Business Logic, Not HTTP Concerns

Services shouldn't know about HTTP requests, responses, or sessions:

php
// Good: Accept data
public function createUser(array $data): array

// Bad: Accept HTTP request
public function createUser(ServerRequestInterface $request): array

5. Throw Exceptions for Business Rule Violations

Use exceptions to signal business rule violations, let controllers handle HTTP responses:

php
public function createUser(array $data): array
{
    if ($this->repository->emailExists($data['email'])) {
        throw new \DomainException('Email already exists');
    }
    
    return $this->repository->create($data);
}

6. Use Type Declarations

Always use strict types and type declarations:

php
declare(strict_types=1);

public function getUserById(int $id): ?array
{
    // Implementation
}

7. Make Services Readonly

Use readonly classes to ensure immutability:

php
final readonly class UserService
{
    public function __construct(
        private UserRepository $repository
    ) {}
}

Testing Services

Services are easy to test because they don't depend on HTTP layer:

php
use PHPUnit\Framework\TestCase;

final class UserServiceTest extends TestCase
{
    public function testGetAllUsers(): void
    {
        $mockRepository = $this->createMock(UserRepository::class);
        $mockRepository->method('findAll')->willReturn([
            ['id' => 1, 'name' => 'John'],
            ['id' => 2, 'name' => 'Jane'],
        ]);
        
        $mockCache = $this->createMock(CacheInterface::class);
        $mockCache->method('get')->willReturn(null);
        
        $service = new UserService($mockRepository, $mockCache);
        $users = $service->getAllUsers();
        
        $this->assertCount(2, $users);
        $this->assertEquals('John', $users[0]['name']);
    }
}

See Also:

12.2 Repository Pattern

The repository pattern provides an abstraction layer between your business logic and data access logic. It encapsulates the logic required to access data sources, providing a clean API for data operations while hiding implementation details.

What is the Repository Pattern?

A repository acts as a collection-like interface for accessing domain objects. It provides methods to retrieve, store, update, and delete entities without exposing the underlying data storage mechanism (database, API, file system, etc.).

Benefits of the Repository Pattern:

  • Abstraction: Business logic doesn't need to know about data storage details
  • Testability: Easy to mock repositories for testing services
  • Flexibility: Can switch data sources without changing business logic
  • Centralization: All data access logic is in one place
  • Reusability: Repositories can be used across multiple services
  • Query Optimization: Centralized location for optimizing data queries

Repository Architecture

Service → Repository Interface → Repository Implementation → Data Source
   ↓             ↓                        ↓                      ↓
Business      Contract              Implementation          Database/API
 Logic                                                      /File/Cache

Services depend on repository interfaces, not concrete implementations. This allows for easy testing and flexibility.

Creating a Repository Interface

Define an interface that describes what operations are available:

php
namespace ElliePHP\Framework\Application\Repositories;

interface UserRepositoryInterface
{
    /**
     * Find all users
     */
    public function findAll(): array;

    /**
     * Find a user by ID
     */
    public function findById(int $id): ?array;

    /**
     * Find a user by email
     */
    public function findByEmail(string $email): ?array;

    /**
     * Create a new user
     */
    public function create(array $data): array;

    /**
     * Update an existing user
     */
    public function update(int $id, array $data): array;

    /**
     * Delete a user
     */
    public function delete(int $id): bool;

    /**
     * Check if email exists
     */
    public function emailExists(string $email): bool;
}

Implementing a Repository

Create a concrete implementation of the interface:

php
namespace ElliePHP\Framework\Application\Repositories;

use PDO;

final readonly class UserRepository implements UserRepositoryInterface
{
    public function __construct(
        private PDO $database
    ) {
    }

    public function findAll(): array
    {
        $stmt = $this->database->query('SELECT * FROM users ORDER BY created_at DESC');
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    public function findById(int $id): ?array
    {
        $stmt = $this->database->prepare('SELECT * FROM users WHERE id = ?');
        $stmt->execute([$id]);
        
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        return $user ?: null;
    }

    public function findByEmail(string $email): ?array
    {
        $stmt = $this->database->prepare('SELECT * FROM users WHERE email = ?');
        $stmt->execute([$email]);
        
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        return $user ?: null;
    }

    public function create(array $data): array
    {
        $stmt = $this->database->prepare(
            'INSERT INTO users (name, email, password, created_at) VALUES (?, ?, ?, ?)'
        );
        
        $stmt->execute([
            $data['name'],
            $data['email'],
            $data['password'],
            date('Y-m-d H:i:s')
        ]);
        
        $id = (int) $this->database->lastInsertId();
        return $this->findById($id);
    }

    public function update(int $id, array $data): array
    {
        $stmt = $this->database->prepare(
            'UPDATE users SET name = ?, email = ?, updated_at = ? WHERE id = ?'
        );
        
        $stmt->execute([
            $data['name'],
            $data['email'],
            date('Y-m-d H:i:s'),
            $id
        ]);
        
        return $this->findById($id);
    }

    public function delete(int $id): bool
    {
        $stmt = $this->database->prepare('DELETE FROM users WHERE id = ?');
        $stmt->execute([$id]);
        
        return $stmt->rowCount() > 0;
    }

    public function emailExists(string $email): bool
    {
        $stmt = $this->database->prepare('SELECT COUNT(*) FROM users WHERE email = ?');
        $stmt->execute([$email]);
        
        return $stmt->fetchColumn() > 0;
    }
}

Binding Repository to Interface

Register the interface-to-implementation binding in configs/Container.php:

php
<?php

use ElliePHP\Framework\Application\Repositories\UserRepositoryInterface;
use ElliePHP\Framework\Application\Repositories\UserRepository;
use function DI\autowire;

return [
    // Bind interface to implementation
    UserRepositoryInterface::class => autowire(UserRepository::class),
    
    // Configure PDO connection
    PDO::class => function () {
        return new PDO(
            env('DB_DSN', 'sqlite:' . storage_path('database.sqlite')),
            env('DB_USER'),
            env('DB_PASS'),
            [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            ]
        );
    },
];

Now services can depend on the interface, and the container will automatically inject the implementation:

php
final readonly class UserService
{
    public function __construct(
        private UserRepositoryInterface $repository // Interface, not implementation
    ) {
    }
}

Repository with Query Builder

For more complex queries, you might use a query builder:

php
namespace ElliePHP\Framework\Application\Repositories;

use PDO;

final readonly class OrderRepository implements OrderRepositoryInterface
{
    public function __construct(
        private PDO $database
    ) {
    }

    public function findByUserId(int $userId, array $filters = []): array
    {
        $query = 'SELECT * FROM orders WHERE user_id = ?';
        $params = [$userId];
        
        // Apply status filter
        if (!empty($filters['status'])) {
            $query .= ' AND status = ?';
            $params[] = $filters['status'];
        }
        
        // Apply date range filter
        if (!empty($filters['from_date'])) {
            $query .= ' AND created_at >= ?';
            $params[] = $filters['from_date'];
        }
        
        if (!empty($filters['to_date'])) {
            $query .= ' AND created_at <= ?';
            $params[] = $filters['to_date'];
        }
        
        // Apply sorting
        $sortBy = $filters['sort_by'] ?? 'created_at';
        $sortDir = $filters['sort_dir'] ?? 'DESC';
        $query .= " ORDER BY {$sortBy} {$sortDir}";
        
        // Apply pagination
        if (!empty($filters['limit'])) {
            $query .= ' LIMIT ?';
            $params[] = (int) $filters['limit'];
            
            if (!empty($filters['offset'])) {
                $query .= ' OFFSET ?';
                $params[] = (int) $filters['offset'];
            }
        }
        
        $stmt = $this->database->prepare($query);
        $stmt->execute($params);
        
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    public function findWithItems(int $orderId): ?array
    {
        // Get order
        $stmt = $this->database->prepare('SELECT * FROM orders WHERE id = ?');
        $stmt->execute([$orderId]);
        $order = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$order) {
            return null;
        }
        
        // Get order items
        $stmt = $this->database->prepare('SELECT * FROM order_items WHERE order_id = ?');
        $stmt->execute([$orderId]);
        $order['items'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
        
        return $order;
    }

    public function getTotalsByStatus(): array
    {
        $stmt = $this->database->query(
            'SELECT status, COUNT(*) as count, SUM(total) as total 
             FROM orders 
             GROUP BY status'
        );
        
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

Repository with Caching

Repositories can implement caching for frequently accessed data:

php
namespace ElliePHP\Framework\Application\Repositories;

use PDO;
use Psr\SimpleCache\CacheInterface;

final readonly class ProductRepository implements ProductRepositoryInterface
{
    public function __construct(
        private PDO $database,
        private CacheInterface $cache
    ) {
    }

    public function findById(int $id): ?array
    {
        $cacheKey = "product:{$id}";
        
        // Try cache first
        $product = $this->cache->get($cacheKey);
        if ($product !== null) {
            return $product;
        }
        
        // Query database
        $stmt = $this->database->prepare('SELECT * FROM products WHERE id = ?');
        $stmt->execute([$id]);
        $product = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if ($product) {
            // Cache for 1 hour
            $this->cache->set($cacheKey, $product, 3600);
        }
        
        return $product ?: null;
    }

    public function update(int $id, array $data): array
    {
        $stmt = $this->database->prepare(
            'UPDATE products SET name = ?, price = ?, updated_at = ? WHERE id = ?'
        );
        
        $stmt->execute([
            $data['name'],
            $data['price'],
            date('Y-m-d H:i:s'),
            $id
        ]);
        
        // Invalidate cache
        $this->cache->delete("product:{$id}");
        $this->cache->delete('products:all');
        
        return $this->findById($id);
    }
}

Multiple Repository Implementations

You can have multiple implementations of the same interface for different data sources:

php
// Database implementation
final readonly class DatabaseUserRepository implements UserRepositoryInterface
{
    public function __construct(private PDO $database) {}
    
    public function findAll(): array
    {
        // Query database
    }
}

// API implementation
final readonly class ApiUserRepository implements UserRepositoryInterface
{
    public function __construct(private HttpClient $client) {}
    
    public function findAll(): array
    {
        // Call external API
    }
}

// In-memory implementation (for testing)
final class InMemoryUserRepository implements UserRepositoryInterface
{
    private array $users = [];
    
    public function findAll(): array
    {
        return $this->users;
    }
    
    public function create(array $data): array
    {
        $data['id'] = count($this->users) + 1;
        $this->users[] = $data;
        return $data;
    }
}

Switch implementations by changing the container binding:

php
// configs/Container.php

// Use database in production
UserRepositoryInterface::class => autowire(DatabaseUserRepository::class),

// Use API in staging
UserRepositoryInterface::class => autowire(ApiUserRepository::class),

// Use in-memory in tests
UserRepositoryInterface::class => autowire(InMemoryUserRepository::class),

Repository Best Practices

1. Use Interfaces

Always define an interface for your repositories:

php
// Good: Interface + Implementation
interface UserRepositoryInterface { }
class UserRepository implements UserRepositoryInterface { }

// Bad: Concrete class only
class UserRepository { }

2. Return Domain Data, Not Database Objects

Return arrays or domain objects, not PDOStatement or database-specific objects:

php
// Good: Return array
public function findById(int $id): ?array

// Bad: Return PDOStatement
public function findById(int $id): PDOStatement

3. Keep Repositories Focused

One repository per entity or aggregate root:

php
// Good: Focused repositories
UserRepository
OrderRepository
ProductRepository

// Bad: Generic repository
GenericRepository // handles all entities

4. Don't Put Business Logic in Repositories

Repositories should only handle data access, not business rules:

php
// Good: Simple data access
public function findById(int $id): ?array
{
    $stmt = $this->database->prepare('SELECT * FROM users WHERE id = ?');
    $stmt->execute([$id]);
    return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}

// Bad: Business logic in repository
public function findById(int $id): ?array
{
    $user = $this->database->prepare('SELECT * FROM users WHERE id = ?');
    $stmt->execute([$id]);
    $user = $stmt->fetch(PDO::FETCH_ASSOC);
    
    // Don't do business logic here
    if ($user && $user['status'] === 'inactive') {
        $this->sendReactivationEmail($user);
    }
    
    return $user;
}

5. Use Type Declarations

Always use strict types and type declarations:

php
declare(strict_types=1);

public function findById(int $id): ?array
{
    // Implementation
}

6. Make Repositories Readonly

Use readonly classes when possible:

php
final readonly class UserRepository implements UserRepositoryInterface
{
    public function __construct(private PDO $database) {}
}

7. Handle Errors Appropriately

Let exceptions bubble up, don't catch and hide them:

php
// Good: Let exceptions propagate
public function create(array $data): array
{
    $stmt = $this->database->prepare('INSERT INTO users ...');
    $stmt->execute($data); // PDOException will propagate
    return $this->findById($this->database->lastInsertId());
}

// Bad: Catch and hide errors
public function create(array $data): ?array
{
    try {
        $stmt = $this->database->prepare('INSERT INTO users ...');
        $stmt->execute($data);
        return $this->findById($this->database->lastInsertId());
    } catch (PDOException $e) {
        return null; // Error is hidden
    }
}

Testing Repositories

Repositories can be tested with a test database or mocked:

php
use PHPUnit\Framework\TestCase;

final class UserRepositoryTest extends TestCase
{
    private PDO $database;
    private UserRepository $repository;
    
    protected function setUp(): void
    {
        // Use in-memory SQLite for testing
        $this->database = new PDO('sqlite::memory:');
        $this->database->exec('
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name TEXT,
                email TEXT UNIQUE,
                created_at TEXT
            )
        ');
        
        $this->repository = new UserRepository($this->database);
    }
    
    public function testFindById(): void
    {
        // Insert test data
        $this->database->exec("
            INSERT INTO users (name, email, created_at) 
            VALUES ('John Doe', 'john@example.com', '2024-01-01 00:00:00')
        ");
        
        $user = $this->repository->findById(1);
        
        $this->assertNotNull($user);
        $this->assertEquals('John Doe', $user['name']);
        $this->assertEquals('john@example.com', $user['email']);
    }
    
    public function testCreate(): void
    {
        $user = $this->repository->create([
            'name' => 'Jane Doe',
            'email' => 'jane@example.com',
            'password' => 'hashed_password'
        ]);
        
        $this->assertIsArray($user);
        $this->assertEquals('Jane Doe', $user['name']);
        $this->assertArrayHasKey('id', $user);
    }
}

See Also:

12.3 Framework Extension Points

ElliePHP is designed to be extended and customized to fit your application's specific needs. The framework provides several extension points where you can add custom functionality without modifying the core framework code.

Overview of Extension Points

ElliePHP can be extended through:

  1. Custom Middleware - Add request/response processing logic
  2. Custom Console Commands - Create CLI tools for your application
  3. Custom Service Providers - Register and configure services
  4. Custom Utilities - Add helper functions and utility classes
  5. Custom Cache Drivers - Implement alternative caching backends
  6. Custom Configuration - Add application-specific configuration files

1. Creating Custom Middleware

Middleware is the primary way to extend request/response processing. Create middleware by implementing the PSR-15 MiddlewareInterface.

Example: Authentication Middleware

php
namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final readonly class AuthMiddleware implements MiddlewareInterface
{
    public function __construct(
        private AuthService $authService
    ) {
    }

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Extract token from Authorization header
        $token = $this->extractToken($request);
        
        if ($token === null) {
            return response()->unauthorized()->json([
                'error' => 'Authentication required'
            ]);
        }
        
        // Validate token
        $user = $this->authService->validateToken($token);
        
        if ($user === null) {
            return response()->unauthorized()->json([
                'error' => 'Invalid or expired token'
            ]);
        }
        
        // Add user to request attributes
        $request = $request->withAttribute('user', $user);
        
        // Continue to next middleware/handler
        return $handler->handle($request);
    }

    private function extractToken(ServerRequestInterface $request): ?string
    {
        $header = $request->getHeaderLine('Authorization');
        
        if (preg_match('/Bearer\s+(.*)$/i', $header, $matches)) {
            return $matches[1];
        }
        
        return null;
    }
}

Example: Rate Limiting Middleware

php
namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\SimpleCache\CacheInterface;

final readonly class RateLimitMiddleware implements MiddlewareInterface
{
    public function __construct(
        private CacheInterface $cache,
        private int $maxRequests = 60,
        private int $windowSeconds = 60
    ) {
    }

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $clientIp = $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown';
        $cacheKey = "rate_limit:{$clientIp}";
        
        // Get current request count
        $requests = (int) $this->cache->get($cacheKey, 0);
        
        if ($requests >= $this->maxRequests) {
            return response()->tooManyRequests()->json([
                'error' => 'Rate limit exceeded',
                'retry_after' => $this->windowSeconds
            ])->withHeader('Retry-After', (string) $this->windowSeconds);
        }
        
        // Increment request count
        $this->cache->set($cacheKey, $requests + 1, $this->windowSeconds);
        
        // Add rate limit headers
        $response = $handler->handle($request);
        
        return $response
            ->withHeader('X-RateLimit-Limit', (string) $this->maxRequests)
            ->withHeader('X-RateLimit-Remaining', (string) ($this->maxRequests - $requests - 1));
    }
}

Register Custom Middleware

Add your middleware to configs/Middleware.php:

php
<?php

use ElliePHP\Framework\Application\Http\Middlewares\AuthMiddleware;
use ElliePHP\Framework\Application\Http\Middlewares\RateLimitMiddleware;
use ElliePHP\Framework\Application\Http\Middlewares\CorsMiddleware;
use ElliePHP\Framework\Application\Http\Middlewares\LoggingMiddleware;

return [
    'global_middlewares' => [
        CorsMiddleware::class,
        LoggingMiddleware::class,
        RateLimitMiddleware::class,
        // AuthMiddleware::class, // Apply globally or per-route
    ],
];

See Also: Creating Custom Middleware

2. Creating Custom Console Commands

Extend the framework's CLI capabilities by creating custom commands.

Example: Database Seed Command

php
namespace ElliePHP\Framework\Application\Console\Command;

use ElliePHP\Components\Console\Command\BaseCommand;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;

final class DatabaseSeedCommand extends BaseCommand
{
    public function __construct(
        private readonly DatabaseSeeder $seeder
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this->setName('db:seed')
            ->setDescription('Seed the database with sample data')
            ->addArgument('seeder', InputArgument::OPTIONAL, 'Specific seeder class to run')
            ->addOption('force', 'f', InputOption::VALUE_NONE, 'Force seeding in production');
    }

    protected function handle(): int
    {
        $seederClass = $this->argument('seeder');
        $force = $this->option('force');
        
        // Warn in production
        if (env('APP_ENV') === 'production' && !$force) {
            $this->error('Cannot seed database in production without --force flag');
            return self::FAILURE;
        }
        
        $this->info('Seeding database...');
        
        try {
            if ($seederClass) {
                $this->seeder->run($seederClass);
                $this->success("Seeder {$seederClass} completed successfully");
            } else {
                $this->seeder->runAll();
                $this->success('All seeders completed successfully');
            }
            
            return self::SUCCESS;
        } catch (\Exception $e) {
            $this->error('Seeding failed: ' . $e->getMessage());
            return self::FAILURE;
        }
    }
}

Example: Cache Warm Command

php
namespace ElliePHP\Framework\Application\Console\Command;

use ElliePHP\Components\Console\Command\BaseCommand;
use Psr\SimpleCache\CacheInterface;

final class CacheWarmCommand extends BaseCommand
{
    public function __construct(
        private readonly CacheInterface $cache,
        private readonly ConfigRepository $config
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this->setName('cache:warm')
            ->setDescription('Warm up the application cache');
    }

    protected function handle(): int
    {
        $this->info('Warming up cache...');
        
        $items = [
            'config' => fn() => $this->config->all(),
            'routes' => fn() => $this->loadRoutes(),
            'settings' => fn() => $this->loadSettings(),
        ];
        
        $progressBar = $this->createProgressBar(count($items));
        
        foreach ($items as $key => $loader) {
            $this->cache->set($key, $loader(), 3600);
            $progressBar->advance();
        }
        
        $progressBar->finish();
        $this->newLine(2);
        $this->success('Cache warmed successfully');
        
        return self::SUCCESS;
    }
}

Register Custom Commands

Add your commands to configs/Commands.php:

php
<?php

use ElliePHP\Framework\Application\Console\Command\DatabaseSeedCommand;
use ElliePHP\Framework\Application\Console\Command\CacheWarmCommand;

return [
    'app' => [
        DatabaseSeedCommand::class,
        CacheWarmCommand::class,
    ],
];

See Also: Creating Custom Commands

3. Creating Custom Service Providers

Service providers are classes that register and configure services in the dependency injection container.

Example: Database Service Provider

php
namespace ElliePHP\Framework\Application\Providers;

use PDO;

final class DatabaseServiceProvider
{
    public static function register(): array
    {
        return [
            PDO::class => function () {
                $dsn = env('DB_DSN', 'sqlite:' . storage_path('database.sqlite'));
                $username = env('DB_USER');
                $password = env('DB_PASS');
                
                $pdo = new PDO($dsn, $username, $password, [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                    PDO::ATTR_EMULATE_PREPARES => false,
                ]);
                
                // Log queries in debug mode
                if (env('APP_DEBUG')) {
                    $pdo->setAttribute(PDO::ATTR_STATEMENT_CLASS, [
                        LoggingPDOStatement::class
                    ]);
                }
                
                return $pdo;
            },
        ];
    }
}

Example: Mail Service Provider

php
namespace ElliePHP\Framework\Application\Providers;

use ElliePHP\Framework\Application\Services\MailService;
use ElliePHP\Framework\Application\Mail\SmtpTransport;
use ElliePHP\Framework\Application\Mail\MailgunTransport;

final class MailServiceProvider
{
    public static function register(): array
    {
        return [
            // Register mail transport based on configuration
            'mail.transport' => function () {
                $driver = env('MAIL_DRIVER', 'smtp');
                
                return match ($driver) {
                    'smtp' => new SmtpTransport(
                        host: env('MAIL_HOST'),
                        port: env('MAIL_PORT', 587),
                        username: env('MAIL_USERNAME'),
                        password: env('MAIL_PASSWORD'),
                        encryption: env('MAIL_ENCRYPTION', 'tls')
                    ),
                    'mailgun' => new MailgunTransport(
                        apiKey: env('MAILGUN_API_KEY'),
                        domain: env('MAILGUN_DOMAIN')
                    ),
                    default => throw new \RuntimeException("Unsupported mail driver: {$driver}")
                };
            },
            
            // Register mail service
            MailService::class => function ($c) {
                return new MailService(
                    transport: $c->get('mail.transport'),
                    from: [
                        'address' => env('MAIL_FROM_ADDRESS'),
                        'name' => env('MAIL_FROM_NAME')
                    ]
                );
            },
        ];
    }
}

Register Service Providers

Add provider bindings to configs/Container.php:

php
<?php

use ElliePHP\Framework\Application\Providers\DatabaseServiceProvider;
use ElliePHP\Framework\Application\Providers\MailServiceProvider;

return array_merge(
    DatabaseServiceProvider::register(),
    MailServiceProvider::register(),
    [
        // Other bindings...
    ]
);

4. Creating Custom Utilities

Add custom helper functions and utility classes for your application.

Example: Custom Helper Functions

Create a file app/Support/helpers.php:

php
<?php

use ElliePHP\Framework\Application\Services\AuthService;

if (!function_exists('auth')) {
    /**
     * Get the authenticated user
     */
    function auth(): ?array
    {
        return container(AuthService::class)->user();
    }
}

if (!function_exists('user')) {
    /**
     * Get the current user or a specific attribute
     */
    function user(?string $key = null): mixed
    {
        $user = auth();
        
        if ($key === null) {
            return $user;
        }
        
        return $user[$key] ?? null;
    }
}

if (!function_exists('can')) {
    /**
     * Check if user has permission
     */
    function can(string $permission): bool
    {
        $user = auth();
        
        if ($user === null) {
            return false;
        }
        
        return in_array($permission, $user['permissions'] ?? []);
    }
}

if (!function_exists('asset')) {
    /**
     * Generate URL for asset
     */
    function asset(string $path): string
    {
        $baseUrl = env('APP_URL', 'http://localhost:8000');
        return rtrim($baseUrl, '/') . '/' . ltrim($path, '/');
    }
}

Load Custom Helpers

Add to your composer.json:

json
{
    "autoload": {
        "files": [
            "src/Support/path.php",
            "src/Support/helpers.php",
            "app/Support/helpers.php"
        ]
    }
}

Then run:

bash
composer dump-autoload

Example: Custom Utility Class

php
namespace ElliePHP\Framework\Application\Support;

final class Arr
{
    /**
     * Get an item from array using dot notation
     */
    public static function get(array $array, string $key, mixed $default = null): mixed
    {
        if (isset($array[$key])) {
            return $array[$key];
        }
        
        foreach (explode('.', $key) as $segment) {
            if (!is_array($array) || !array_key_exists($segment, $array)) {
                return $default;
            }
            $array = $array[$segment];
        }
        
        return $array;
    }

    /**
     * Flatten a multi-dimensional array
     */
    public static function flatten(array $array): array
    {
        $result = [];
        
        array_walk_recursive($array, function ($value) use (&$result) {
            $result[] = $value;
        });
        
        return $result;
    }

    /**
     * Filter array by callback
     */
    public static function where(array $array, callable $callback): array
    {
        return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH);
    }
}

5. Creating Custom Cache Drivers

Implement custom cache drivers for alternative storage backends.

Example: Memcached Cache Driver

php
namespace ElliePHP\Framework\Application\Cache;

use Psr\SimpleCache\CacheInterface;
use Memcached;

final class MemcachedDriver implements CacheInterface
{
    private Memcached $memcached;
    private string $prefix;

    public function __construct(array $config = [])
    {
        $this->memcached = new Memcached();
        $this->memcached->addServer(
            $config['host'] ?? '127.0.0.1',
            $config['port'] ?? 11211
        );
        
        $this->prefix = $config['prefix'] ?? 'ellie_cache:';
    }

    public function get(string $key, mixed $default = null): mixed
    {
        $value = $this->memcached->get($this->prefix . $key);
        
        return $value !== false ? $value : $default;
    }

    public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool
    {
        $expiration = $this->calculateExpiration($ttl);
        
        return $this->memcached->set($this->prefix . $key, $value, $expiration);
    }

    public function delete(string $key): bool
    {
        return $this->memcached->delete($this->prefix . $key);
    }

    public function clear(): bool
    {
        return $this->memcached->flush();
    }

    public function has(string $key): bool
    {
        return $this->memcached->get($this->prefix . $key) !== false;
    }

    // Implement other PSR-16 methods...
}

Register Custom Cache Driver

php
// configs/Container.php

use Psr\SimpleCache\CacheInterface;
use ElliePHP\Framework\Application\Cache\MemcachedDriver;

return [
    CacheInterface::class => function () {
        $driver = env('CACHE_DRIVER', 'file');
        
        return match ($driver) {
            'memcached' => new MemcachedDriver([
                'host' => env('MEMCACHED_HOST', '127.0.0.1'),
                'port' => env('MEMCACHED_PORT', 11211),
            ]),
            default => CacheFactory::createFileDriver()
        };
    },
];

6. Creating Custom Configuration Files

Add application-specific configuration files.

Example: API Configuration

Create configs/Api.php:

php
<?php

return [
    'version' => 'v1',
    
    'rate_limit' => [
        'enabled' => env('API_RATE_LIMIT_ENABLED', true),
        'max_requests' => env('API_RATE_LIMIT_MAX', 60),
        'window_seconds' => env('API_RATE_LIMIT_WINDOW', 60),
    ],
    
    'authentication' => [
        'driver' => env('API_AUTH_DRIVER', 'jwt'),
        'token_ttl' => env('API_TOKEN_TTL', 3600),
    ],
    
    'pagination' => [
        'default_per_page' => 15,
        'max_per_page' => 100,
    ],
    
    'cors' => [
        'allowed_origins' => explode(',', env('API_CORS_ORIGINS', '*')),
        'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
        'allowed_headers' => ['Content-Type', 'Authorization'],
    ],
];

Access Custom Configuration

php
// Get entire config
$apiConfig = config('Api');

// Get specific value with dot notation
$maxRequests = config('Api.rate_limit.max_requests');
$authDriver = config('Api.authentication.driver');

Extension Best Practices

1. Follow Framework Conventions

  • Use PSR standards (PSR-15 for middleware, PSR-16 for cache)
  • Follow naming conventions (Command suffix for commands, Middleware suffix for middleware)
  • Use proper namespaces under ElliePHP\Framework\Application\

2. Use Dependency Injection

Always inject dependencies through constructors:

php
public function __construct(
    private readonly UserRepository $repository,
    private readonly CacheInterface $cache
) {}

3. Make Extensions Configurable

Use environment variables and configuration files:

php
$maxRequests = env('RATE_LIMIT_MAX', 60);
$enabled = config('Api.rate_limit.enabled', true);

4. Document Your Extensions

Add docblocks and comments explaining what your extension does:

php
/**
 * Rate limiting middleware
 * 
 * Limits the number of requests per IP address within a time window.
 * Configure via RATE_LIMIT_MAX and RATE_LIMIT_WINDOW env variables.
 */
final readonly class RateLimitMiddleware implements MiddlewareInterface

5. Write Tests

Test your extensions to ensure they work correctly:

php
final class RateLimitMiddlewareTest extends TestCase
{
    public function testAllowsRequestsWithinLimit(): void
    {
        // Test implementation
    }
}

6. Keep Extensions Focused

Each extension should have a single, clear purpose. Don't create monolithic extensions that do too much.

See Also:

12.4 Production Optimization

Optimizing ElliePHP for production environments ensures maximum performance, security, and reliability. This section covers essential optimizations and best practices for deploying your application to production.

Overview of Production Optimizations

Key areas for production optimization:

  1. Container Compilation - Pre-compile dependency injection container
  2. Route Caching - Cache route definitions for faster matching
  3. Cache Driver Selection - Choose optimal cache backend
  4. PHP Configuration - Optimize PHP settings for production
  5. Autoloader Optimization - Generate optimized class maps
  6. Error Handling - Disable debug mode and configure logging

1. Container Compilation

PHP-DI can compile the container to generate optimized PHP code, eliminating reflection overhead and improving performance by ~40%.

Enable Container Compilation

Set the APP_ENV environment variable to production in your .env file:

env
APP_ENV=production
APP_DEBUG=false

When APP_ENV=production, PHP-DI automatically compiles the container and stores it in storage/Cache/CompiledContainer.php.

How Container Compilation Works

In development mode:

  • Container uses reflection to resolve dependencies at runtime
  • Slower but allows for dynamic changes without cache clearing

In production mode:

  • Container generates optimized PHP code at first request
  • Subsequent requests use the compiled container (no reflection)
  • Much faster but requires cache clearing after code changes

Compiled Container Location

storage/
└── Cache/
    └── CompiledContainer.php  # Generated container code

Clear Compiled Container

After deploying code changes, clear the compiled container:

bash
php ellie cache:clear --config

Or manually delete the file:

bash
rm storage/Cache/CompiledContainer.php

Container Compilation Benefits

  • 40% faster dependency resolution
  • Reduced memory usage (no reflection metadata)
  • Better opcode caching (static PHP code)
  • Improved startup time for each request

Example: Container Configuration for Production

php
// configs/Container.php

use function DI\autowire;
use function DI\create;

return [
    // Use create() with lazy() for singletons in production
    CacheInterface::class => create(FileCache::class)->lazy(),
    
    // Autowire for automatic dependency resolution
    UserService::class => autowire(),
    
    // Factory for complex initialization
    PDO::class => function () {
        return new PDO(
            env('DB_DSN'),
            env('DB_USER'),
            env('DB_PASS'),
            [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_PERSISTENT => true, // Use persistent connections in production
            ]
        );
    },
];

2. Route Caching

Cache route definitions to eliminate route file parsing on every request.

Enable Route Caching

Routes are automatically cached when using the file cache driver. The cache file is stored at:

storage/Cache/ellie_routes_[hash].cache

Clear Route Cache

After modifying routes, clear the cache:

bash
php ellie cache:clear --routes

Route Caching Benefits

  • Faster route matching (no file parsing)
  • Reduced I/O operations
  • Lower memory usage

Note: Route caching is automatic and doesn't require configuration. The framework handles cache invalidation intelligently.

3. Cache Driver Selection

Choose the optimal cache driver for your production environment.

Cache Driver Comparison

DriverSpeedPersistenceMulti-ServerBest For
APCuFastestNo (memory)NoSingle-server, high-performance
RedisVery FastYesYesMulti-server, distributed systems
FileFastYesNoSimple deployments, shared hosting
SQLiteModerateYesNoEmbedded applications

Recommended Production Drivers

For Single-Server Deployments:

Use APCu for maximum performance:

env
CACHE_DRIVER=apcu

Install APCu extension:

bash
pecl install apcu

Enable in php.ini:

ini
extension=apcu.so
apc.enabled=1
apc.shm_size=64M
apc.ttl=7200
apc.gc_ttl=3600

For Multi-Server Deployments:

Use Redis for shared caching across servers:

env
CACHE_DRIVER=redis
REDIS_HOST=your-redis-server.com
REDIS_PORT=6379
REDIS_PASSWORD=your-secure-password
REDIS_DATABASE=0

For Shared Hosting:

Use file cache (default):

env
CACHE_DRIVER=file

Ensure storage/Cache/ is writable:

bash
chmod -R 775 storage/Cache

Cache Configuration Example

php
// configs/Container.php

use Psr\SimpleCache\CacheInterface;
use ElliePHP\Components\Cache\CacheFactory;

return [
    CacheInterface::class => function () {
        $driver = env('CACHE_DRIVER', 'file');
        
        return match ($driver) {
            'apcu' => CacheFactory::createApcuDriver([
                'prefix' => env('CACHE_PREFIX', 'ellie_'),
            ]),
            'redis' => CacheFactory::createRedisDriver([
                'host' => env('REDIS_HOST', '127.0.0.1'),
                'port' => env('REDIS_PORT', 6379),
                'password' => env('REDIS_PASSWORD'),
                'database' => env('REDIS_DATABASE', 0),
                'timeout' => env('REDIS_TIMEOUT', 5),
                'prefix' => env('CACHE_PREFIX', 'ellie_'),
            ]),
            'file' => CacheFactory::createFileDriver([
                'path' => storage_cache_path(),
                'create_directory' => true,
            ]),
            default => throw new \RuntimeException("Unsupported cache driver: {$driver}")
        };
    },
];

4. PHP Configuration

Optimize PHP settings for production performance.

Recommended php.ini Settings

ini
; Error Handling
display_errors = Off
display_startup_errors = Off
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
log_errors = On
error_log = /var/log/php/error.log

; Performance
memory_limit = 256M
max_execution_time = 30
max_input_time = 60

; OPcache (Critical for Production)
opcache.enable = 1
opcache.enable_cli = 0
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 10000
opcache.revalidate_freq = 60
opcache.fast_shutdown = 1
opcache.validate_timestamps = 0  ; Disable in production for max performance

; Realpath Cache
realpath_cache_size = 4096K
realpath_cache_ttl = 600

; Session (if using sessions)
session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379"
session.gc_maxlifetime = 3600

; File Uploads
upload_max_filesize = 10M
post_max_size = 10M
max_file_uploads = 20

OPcache Configuration

OPcache is critical for production performance. It caches compiled PHP bytecode in memory.

Enable OPcache:

bash
# Check if OPcache is enabled
php -i | grep opcache.enable

# If not enabled, add to php.ini
zend_extension=opcache.so
opcache.enable=1

OPcache Benefits:

  • 5-10x faster PHP execution
  • Reduced CPU usage (no recompilation)
  • Lower disk I/O (bytecode cached in memory)

Clear OPcache After Deployment:

bash
# Restart PHP-FPM
sudo systemctl restart php8.4-fpm

# Or use a script to clear OPcache
php -r "opcache_reset();"

5. Autoloader Optimization

Generate optimized Composer autoloader for faster class loading.

Optimize Autoloader

Run during deployment:

bash
composer install --no-dev --optimize-autoloader --classmap-authoritative

Flags Explained:

  • --no-dev: Don't install development dependencies
  • --optimize-autoloader: Generate optimized class map
  • --classmap-authoritative: Use only class map (no file system checks)

Autoloader Optimization Benefits:

  • Faster class loading (no file system lookups)
  • Reduced I/O operations
  • Better opcode caching

Update Autoloader After Code Changes:

bash
composer dump-autoload --optimize --classmap-authoritative

6. Error Handling and Logging

Configure proper error handling for production.

Disable Debug Mode

env
APP_ENV=production
APP_DEBUG=false

When debug mode is disabled:

  • Detailed error messages are hidden from users
  • Errors are logged to files instead of displayed
  • Stack traces are not exposed in responses

Configure Logging

Ensure logs are written to persistent storage:

php
// Logs are automatically written to:
storage/Logs/app.log         # Application logs
storage/Logs/exceptions.log  # Exception logs

Log Rotation

Set up log rotation to prevent disk space issues:

bash
# /etc/logrotate.d/elliephp
/path/to/your/app/storage/Logs/*.log {
    daily
    rotate 14
    compress
    delaycompress
    notifempty
    missingok
    create 0644 www-data www-data
}

Error Monitoring

Consider integrating error monitoring services:

php
// Example: Sentry integration
use Sentry\Laravel\Integration;

report()->pushHandler(new SentryHandler(
    dsn: env('SENTRY_DSN'),
    level: \Monolog\Logger::ERROR
));

7. Storage Directory Optimization

Optimize the storage directory structure and permissions.

Directory Structure

storage/
├── Cache/              # Cache files and compiled container
│   ├── CompiledContainer.php
│   └── ellie_routes_*.cache
└── Logs/               # Application logs
    ├── app.log
    └── exceptions.log

Set Proper Permissions

bash
# Make storage writable by web server
chmod -R 775 storage
chown -R www-data:www-data storage

# Secure permissions (more restrictive)
find storage -type d -exec chmod 755 {} \;
find storage -type f -exec chmod 644 {} \;

Clear All Caches

bash
# Clear all caches at once
php ellie cache:clear --all

# Or individually
php ellie cache:clear --config   # Container cache
php ellie cache:clear --routes   # Route cache

8. Production Deployment Checklist

Use this checklist when deploying to production:

Environment Configuration

  • [ ] Set APP_ENV=production
  • [ ] Set APP_DEBUG=false
  • [ ] Configure proper APP_TIMEZONE
  • [ ] Set secure APP_URL
  • [ ] Configure production cache driver (APCu or Redis)
  • [ ] Set database credentials
  • [ ] Configure Redis if using

PHP Configuration

  • [ ] Enable OPcache
  • [ ] Set opcache.validate_timestamps=0
  • [ ] Disable display_errors
  • [ ] Configure memory_limit appropriately
  • [ ] Set up error logging

Composer

  • [ ] Run composer install --no-dev --optimize-autoloader --classmap-authoritative
  • [ ] Verify no dev dependencies are installed

Caching

  • [ ] Clear all caches: php ellie cache:clear --all
  • [ ] Verify container compilation is working
  • [ ] Test cache driver connectivity

File Permissions

  • [ ] Set proper permissions on storage/ directory
  • [ ] Ensure web server can write to storage/Cache/ and storage/Logs/
  • [ ] Verify .env file is not publicly accessible

Security

  • [ ] Ensure .env is not in version control
  • [ ] Configure CORS properly in CorsMiddleware
  • [ ] Set up HTTPS/TLS
  • [ ] Configure rate limiting if needed
  • [ ] Review and secure all API endpoints

Monitoring

  • [ ] Set up log rotation
  • [ ] Configure error monitoring (Sentry, Bugsnag, etc.)
  • [ ] Set up application monitoring
  • [ ] Configure health check endpoints

Testing

  • [ ] Test application in production-like environment
  • [ ] Verify all routes work correctly
  • [ ] Test cache functionality
  • [ ] Verify logging is working
  • [ ] Load test critical endpoints

9. Performance Monitoring

Monitor your application's performance in production.

Key Metrics to Track

  • Response Time: Average time to handle requests
  • Throughput: Requests per second
  • Error Rate: Percentage of failed requests
  • Cache Hit Rate: Percentage of cache hits vs misses
  • Memory Usage: PHP memory consumption
  • CPU Usage: Server CPU utilization

Simple Performance Logging

php
// Add to LoggingMiddleware or create PerformanceMiddleware

$startTime = microtime(true);
$startMemory = memory_get_usage();

$response = $handler->handle($request);

$duration = (microtime(true) - $startTime) * 1000; // ms
$memory = memory_get_usage() - $startMemory;

report()->info('Request completed', [
    'method' => $request->getMethod(),
    'path' => $request->getUri()->getPath(),
    'status' => $response->getStatusCode(),
    'duration_ms' => round($duration, 2),
    'memory_mb' => round($memory / 1024 / 1024, 2),
]);

Cache Statistics

Monitor cache performance:

php
$cache = cache();

// Get cache statistics
$stats = [
    'count' => $cache->count(),
    'size' => $cache->size(),
];

report()->info('Cache statistics', $stats);

10. Production Optimization Best Practices

1. Always Use OPcache

OPcache provides the single biggest performance improvement. Never run production without it.

2. Choose the Right Cache Driver

  • Single server: APCu
  • Multiple servers: Redis
  • Shared hosting: File

3. Optimize Database Queries

  • Use indexes on frequently queried columns
  • Implement query caching in repositories
  • Use connection pooling

4. Implement HTTP Caching

php
return response()
    ->json($data)
    ->withHeader('Cache-Control', 'public, max-age=3600')
    ->withHeader('ETag', md5(json_encode($data)));

5. Use Persistent Connections

For database and Redis connections:

php
$pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_PERSISTENT => true,
]);

6. Minimize Autoloaded Files

Only autoload files that are needed on every request.

7. Profile and Benchmark

Use tools like Blackfire, Xdebug, or New Relic to identify bottlenecks.

8. Keep Dependencies Updated

Regularly update Composer dependencies for performance improvements and security fixes:

bash
composer update --no-dev

9. Use HTTP/2

Configure your web server to use HTTP/2 for better performance.

10. Implement CDN

Use a CDN for static assets to reduce server load and improve response times.

See Also:

12.5 Error Handling Strategies

ElliePHP provides a robust error handling system that catches and processes exceptions throughout the application lifecycle. Understanding how errors are handled helps you build more reliable applications and debug issues effectively.

Overview of Error Handling

ElliePHP's error handling system consists of:

  1. Global Exception Handler - Catches all uncaught exceptions
  2. Bootstrap Exception Handling - Handles errors during application startup
  3. Runtime Exception Handling - Handles errors during request processing
  4. Debug vs Production Modes - Different error responses based on environment
  5. Exception Logging - Automatic logging of all exceptions

Global Exception Handler

The Kernel class registers a global exception handler that catches all uncaught exceptions in your application.

How It Works

php
// src/Kernel/Kernel.php

private function registerGlobalExceptionHandler(): void
{
    set_exception_handler(function (Throwable $e): void {
        $this->handleException($e);
    });
}

This handler:

  • Catches all uncaught exceptions
  • Logs exceptions to the exception log
  • Returns appropriate error responses
  • Handles both CLI and HTTP contexts

Exception Handler Flow

Exception Thrown

Global Handler Catches

Log to exceptions.log

Determine Context (CLI/HTTP)

Check Debug Mode

Return Appropriate Response

Bootstrap vs Runtime Exceptions

ElliePHP distinguishes between exceptions that occur during bootstrap (application startup) and runtime (request processing).

Bootstrap Exceptions

Occur during application initialization:

  • Environment file not found
  • Required environment variables missing
  • Configuration errors
  • Container compilation errors

Example Bootstrap Exception:

php
// Kernel detects bootstrap failure
if (!file_exists($envFile)) {
    throw new RuntimeException(
        "Environment file not found at: $envFile\n" .
        "Please copy .env.example to .env and configure your application."
    );
}

Bootstrap Exception Response:

json
{
    "code": 500,
    "status": "Application Bootstrap Exception",
    "message": "Environment configuration error. Please check your logs."
}

Runtime Exceptions

Occur during normal request processing:

  • Database connection errors
  • Invalid user input
  • Business logic violations
  • External API failures

Example Runtime Exception:

php
// Service throws exception
public function getUserById(int $id): array
{
    $user = $this->repository->findById($id);
    
    if ($user === null) {
        throw new NotFoundException("User not found with ID: {$id}");
    }
    
    return $user;
}

Runtime Exception Response:

json
{
    "code": 500,
    "status": "error",
    "message": "An unexpected error occurred"
}

Debug Mode vs Production Mode

Error responses differ based on the APP_ENV environment variable.

Debug Mode (Development)

Enable debug mode in .env:

env
APP_ENV=debug
APP_DEBUG=true

Debug Mode Behavior:

  • Detailed error messages shown to users
  • Full stack traces included in responses
  • Exception details visible in JSON responses
  • Helpful for development and debugging

Debug Mode Response Example:

json
{
    "code": 500,
    "status": "error",
    "message": "SQLSTATE[42S02]: Base table or view not found: 1146 Table 'app.users' doesn't exist",
    "file": "/app/src/Repositories/UserRepository.php",
    "line": 45,
    "trace": [
        "#0 /app/src/Services/UserService.php(23): UserRepository->findAll()",
        "#1 /app/src/Controllers/UserController.php(15): UserService->getAllUsers()",
        "..."
    ]
}

Production Mode

Enable production mode in .env:

env
APP_ENV=production
APP_DEBUG=false

Production Mode Behavior:

  • Generic error messages shown to users
  • No stack traces or sensitive information exposed
  • Detailed errors logged to files
  • Secure for production environments

Production Mode Response Example:

json
{
    "code": 500,
    "status": "error",
    "message": "An unexpected error occurred"
}

Checking Debug Mode in Code:

php
if ($this->isDebugMode()) {
    // Show detailed error information
} else {
    // Show generic error message
}

Exception Logging

All exceptions are automatically logged to storage/Logs/exceptions.log.

Exception Log Format:

[2024-11-17 10:30:45] app.ERROR: User not found with ID: 123 
{
    "exception": "App\\Exceptions\\NotFoundException",
    "file": "/app/src/Services/UserService.php",
    "line": 45,
    "trace": [...]
}

[2024-11-17 10:31:12] app.CRITICAL: Database connection failed 
{
    "exception": "PDOException",
    "message": "SQLSTATE[HY000] [2002] Connection refused",
    "file": "/app/configs/Container.php",
    "line": 23
}

Manual Exception Logging:

php
try {
    $result = $this->riskyOperation();
} catch (Exception $e) {
    report()->exception($e);
    throw $e; // Re-throw if needed
}

Custom Exception Handling

Create custom exception classes for different error types.

Example: Custom Exception Classes

php
namespace ElliePHP\Framework\Application\Exceptions;

// Base application exception
class ApplicationException extends \RuntimeException
{
    public function __construct(
        string $message = '',
        int $code = 500,
        ?\Throwable $previous = null
    ) {
        parent::__construct($message, $code, $previous);
    }
}

// Not found exception (404)
class NotFoundException extends ApplicationException
{
    public function __construct(string $message = 'Resource not found')
    {
        parent::__construct($message, 404);
    }
}

// Validation exception (422)
class ValidationException extends ApplicationException
{
    public function __construct(
        string $message = 'Validation failed',
        private array $errors = []
    ) {
        parent::__construct($message, 422);
    }
    
    public function getErrors(): array
    {
        return $this->errors;
    }
}

// Unauthorized exception (401)
class UnauthorizedException extends ApplicationException
{
    public function __construct(string $message = 'Unauthorized')
    {
        parent::__construct($message, 401);
    }
}

// Forbidden exception (403)
class ForbiddenException extends ApplicationException
{
    public function __construct(string $message = 'Forbidden')
    {
        parent::__construct($message, 403);
    }
}

Using Custom Exceptions:

php
namespace ElliePHP\Framework\Application\Services;

use ElliePHP\Framework\Application\Exceptions\NotFoundException;
use ElliePHP\Framework\Application\Exceptions\ValidationException;

final readonly class UserService
{
    public function getUserById(int $id): array
    {
        $user = $this->repository->findById($id);
        
        if ($user === null) {
            throw new NotFoundException("User not found with ID: {$id}");
        }
        
        return $user;
    }
    
    public function createUser(array $data): array
    {
        $errors = $this->validateUserData($data);
        
        if (!empty($errors)) {
            throw new ValidationException('User validation failed', $errors);
        }
        
        return $this->repository->create($data);
    }
}

Handling Custom Exceptions in Controllers:

php
namespace ElliePHP\Framework\Application\Http\Controllers;

use ElliePHP\Framework\Application\Exceptions\NotFoundException;
use ElliePHP\Framework\Application\Exceptions\ValidationException;
use Psr\Http\Message\ResponseInterface;

final readonly class UserController
{
    public function show(int $id): ResponseInterface
    {
        try {
            $user = $this->userService->getUserById($id);
            return response()->json(['user' => $user]);
        } catch (NotFoundException $e) {
            return response()->notFound()->json([
                'error' => $e->getMessage()
            ]);
        }
    }
    
    public function store(ServerRequestInterface $request): ResponseInterface
    {
        try {
            $data = $request->getParsedBody();
            $user = $this->userService->createUser($data);
            
            return response()->created()->json([
                'message' => 'User created successfully',
                'user' => $user
            ]);
        } catch (ValidationException $e) {
            return response()->unprocessable()->json([
                'error' => $e->getMessage(),
                'errors' => $e->getErrors()
            ]);
        }
    }
}

Exception Handler Middleware

Create middleware to handle exceptions and return consistent error responses.

Example: Exception Handler Middleware

php
namespace ElliePHP\Framework\Application\Http\Middlewares;

use ElliePHP\Framework\Application\Exceptions\ApplicationException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;

final readonly class ExceptionHandlerMiddleware implements MiddlewareInterface
{
    public function __construct(
        private LoggerInterface $logger,
        private bool $debug = false
    ) {
    }

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        try {
            return $handler->handle($request);
        } catch (ApplicationException $e) {
            // Handle known application exceptions
            return $this->handleApplicationException($e);
        } catch (\Throwable $e) {
            // Handle unexpected exceptions
            return $this->handleUnexpectedException($e);
        }
    }

    private function handleApplicationException(ApplicationException $e): ResponseInterface
    {
        $this->logger->error($e->getMessage(), [
            'exception' => get_class($e),
            'code' => $e->getCode(),
        ]);
        
        $response = [
            'error' => $e->getMessage(),
            'code' => $e->getCode(),
        ];
        
        if ($this->debug) {
            $response['file'] = $e->getFile();
            $response['line'] = $e->getLine();
            $response['trace'] = $e->getTrace();
        }
        
        return response($e->getCode())->json($response);
    }

    private function handleUnexpectedException(\Throwable $e): ResponseInterface
    {
        $this->logger->critical('Unexpected exception', [
            'exception' => get_class($e),
            'message' => $e->getMessage(),
            'file' => $e->getFile(),
            'line' => $e->getLine(),
        ]);
        
        $message = $this->debug 
            ? $e->getMessage() 
            : 'An unexpected error occurred';
        
        $response = [
            'error' => $message,
            'code' => 500,
        ];
        
        if ($this->debug) {
            $response['exception'] = get_class($e);
            $response['file'] = $e->getFile();
            $response['line'] = $e->getLine();
            $response['trace'] = $e->getTrace();
        }
        
        return response()->serverError()->json($response);
    }
}

Register Exception Handler Middleware:

php
// configs/Middleware.php

return [
    'global_middlewares' => [
        ExceptionHandlerMiddleware::class,
        CorsMiddleware::class,
        LoggingMiddleware::class,
    ],
];

Error Response Patterns

Consistent error response patterns improve API usability.

Standard Error Response Format:

json
{
    "error": "Human-readable error message",
    "code": 404,
    "details": {
        "field": "email",
        "reason": "Email already exists"
    }
}

Validation Error Response:

json
{
    "error": "Validation failed",
    "code": 422,
    "errors": {
        "email": ["Email is required", "Email must be valid"],
        "password": ["Password must be at least 8 characters"]
    }
}

Helper Function for Error Responses:

php
function errorResponse(
    string $message,
    int $code = 500,
    array $details = []
): ResponseInterface {
    $response = [
        'error' => $message,
        'code' => $code,
    ];
    
    if (!empty($details)) {
        $response['details'] = $details;
    }
    
    return response($code)->json($response);
}

// Usage
return errorResponse('User not found', 404);
return errorResponse('Validation failed', 422, ['errors' => $errors]);

Database Error Handling

Handle database errors gracefully.

Example: Repository Error Handling

php
namespace ElliePHP\Framework\Application\Repositories;

use PDO;
use PDOException;

final readonly class UserRepository
{
    public function findById(int $id): ?array
    {
        try {
            $stmt = $this->database->prepare('SELECT * FROM users WHERE id = ?');
            $stmt->execute([$id]);
            
            $user = $stmt->fetch(PDO::FETCH_ASSOC);
            return $user ?: null;
        } catch (PDOException $e) {
            report()->error('Database query failed', [
                'query' => 'SELECT * FROM users WHERE id = ?',
                'params' => [$id],
                'error' => $e->getMessage(),
            ]);
            
            throw new \RuntimeException('Failed to fetch user from database', 0, $e);
        }
    }
}

External API Error Handling

Handle errors when calling external APIs.

Example: API Client Error Handling

php
namespace ElliePHP\Framework\Application\Services;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;

final readonly class ExternalApiService
{
    public function __construct(
        private Client $httpClient
    ) {
    }

    public function fetchUserData(int $userId): array
    {
        try {
            $response = $this->httpClient->get("/api/users/{$userId}");
            return json_decode($response->getBody()->getContents(), true);
        } catch (GuzzleException $e) {
            report()->error('External API request failed', [
                'endpoint' => "/api/users/{$userId}",
                'error' => $e->getMessage(),
            ]);
            
            throw new \RuntimeException(
                'Failed to fetch user data from external API',
                0,
                $e
            );
        }
    }
}

Error Handling Best Practices

1. Use Specific Exception Types

Create custom exceptions for different error scenarios:

php
// Good: Specific exceptions
throw new NotFoundException('User not found');
throw new ValidationException('Invalid email format');
throw new UnauthorizedException('Invalid credentials');

// Bad: Generic exceptions
throw new Exception('Error occurred');

2. Log Before Throwing

Log errors before throwing exceptions for better debugging:

php
report()->error('Failed to process payment', [
    'user_id' => $userId,
    'amount' => $amount,
]);

throw new PaymentException('Payment processing failed');

3. Don't Catch and Hide Errors

Let exceptions propagate unless you can handle them meaningfully:

php
// Good: Let exception propagate
public function getUser(int $id): array
{
    return $this->repository->findById($id);
}

// Bad: Catch and hide
public function getUser(int $id): ?array
{
    try {
        return $this->repository->findById($id);
    } catch (Exception $e) {
        return null; // Error is hidden
    }
}

4. Provide Context in Error Messages

Include relevant information in error messages:

php
// Good: Contextual message
throw new NotFoundException("User not found with ID: {$id}");

// Bad: Generic message
throw new NotFoundException("Not found");

5. Use Try-Catch Sparingly

Only catch exceptions when you can handle them appropriately:

php
// Good: Specific handling
try {
    $user = $this->userService->createUser($data);
} catch (ValidationException $e) {
    return response()->unprocessable()->json([
        'errors' => $e->getErrors()
    ]);
}

// Bad: Catching everything
try {
    $user = $this->userService->createUser($data);
} catch (Exception $e) {
    // What do we do here?
}

6. Clean Up Resources

Use finally blocks to clean up resources:

php
$file = fopen($path, 'r');
try {
    $content = fread($file, filesize($path));
    // Process content
} finally {
    fclose($file); // Always close file
}

7. Return Appropriate HTTP Status Codes

Use correct status codes for different error types:

  • 400: Bad Request (invalid input)
  • 401: Unauthorized (authentication required)
  • 403: Forbidden (insufficient permissions)
  • 404: Not Found (resource doesn't exist)
  • 422: Unprocessable Entity (validation failed)
  • 500: Internal Server Error (unexpected error)

See Also:

12.6 Security Best Practices

Security is critical for any web application. ElliePHP provides tools and patterns to help you build secure applications, but you must implement security best practices throughout your code. This section covers essential security considerations.

Overview of Security Concerns

Key security areas to address:

  1. CORS Configuration - Control cross-origin requests
  2. Authentication - Verify user identity
  3. Authorization - Control access to resources
  4. Input Validation - Sanitize and validate user input
  5. Password Security - Secure password storage
  6. SQL Injection Prevention - Protect against database attacks
  7. XSS Prevention - Prevent cross-site scripting
  8. CSRF Protection - Prevent cross-site request forgery
  9. Rate Limiting - Prevent abuse and DoS attacks
  10. HTTPS/TLS - Encrypt data in transit

1. CORS Configuration

Cross-Origin Resource Sharing (CORS) controls which domains can access your API.

Default CORS Middleware

ElliePHP includes a basic CORS middleware:

php
// app/Http/Middlewares/CorsMiddleware.php

final class CorsMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $response = $handler->handle($request);

        return $response
            ->withHeader('Access-Control-Allow-Origin', '*')
            ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
            ->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    }
}

⚠️ Security Warning: The default configuration allows all origins (*). This is insecure for production.

Secure CORS Configuration

Create a configurable CORS middleware:

php
namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final readonly class SecureCorsMiddleware implements MiddlewareInterface
{
    private array $allowedOrigins;
    private array $allowedMethods;
    private array $allowedHeaders;
    private bool $allowCredentials;
    private int $maxAge;

    public function __construct()
    {
        $this->allowedOrigins = explode(',', env('CORS_ALLOWED_ORIGINS', ''));
        $this->allowedMethods = explode(',', env('CORS_ALLOWED_METHODS', 'GET,POST,PUT,DELETE'));
        $this->allowedHeaders = explode(',', env('CORS_ALLOWED_HEADERS', 'Content-Type,Authorization'));
        $this->allowCredentials = env('CORS_ALLOW_CREDENTIALS', false);
        $this->maxAge = env('CORS_MAX_AGE', 3600);
    }

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $origin = $request->getHeaderLine('Origin');
        
        // Handle preflight requests
        if ($request->getMethod() === 'OPTIONS') {
            return $this->handlePreflight($origin);
        }
        
        $response = $handler->handle($request);
        
        // Add CORS headers if origin is allowed
        if ($this->isOriginAllowed($origin)) {
            $response = $response
                ->withHeader('Access-Control-Allow-Origin', $origin)
                ->withHeader('Access-Control-Allow-Methods', implode(', ', $this->allowedMethods))
                ->withHeader('Access-Control-Allow-Headers', implode(', ', $this->allowedHeaders));
            
            if ($this->allowCredentials) {
                $response = $response->withHeader('Access-Control-Allow-Credentials', 'true');
            }
        }
        
        return $response;
    }

    private function isOriginAllowed(string $origin): bool
    {
        if (empty($origin)) {
            return false;
        }
        
        // Check if origin is in allowed list
        return in_array($origin, $this->allowedOrigins) || in_array('*', $this->allowedOrigins);
    }

    private function handlePreflight(string $origin): ResponseInterface
    {
        $response = response()->noContent();
        
        if ($this->isOriginAllowed($origin)) {
            $response = $response
                ->withHeader('Access-Control-Allow-Origin', $origin)
                ->withHeader('Access-Control-Allow-Methods', implode(', ', $this->allowedMethods))
                ->withHeader('Access-Control-Allow-Headers', implode(', ', $this->allowedHeaders))
                ->withHeader('Access-Control-Max-Age', (string) $this->maxAge);
            
            if ($this->allowCredentials) {
                $response = $response->withHeader('Access-Control-Allow-Credentials', 'true');
            }
        }
        
        return $response;
    }
}

Environment Configuration:

env
# .env
CORS_ALLOWED_ORIGINS=https://yourdomain.com,https://app.yourdomain.com
CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
CORS_ALLOWED_HEADERS=Content-Type,Authorization,X-Requested-With
CORS_ALLOW_CREDENTIALS=true
CORS_MAX_AGE=3600

2. Authentication Middleware

Implement authentication to verify user identity.

JWT Authentication Middleware Example

php
namespace ElliePHP\Framework\Application\Http\Middlewares;

use ElliePHP\Framework\Application\Services\AuthService;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final readonly class AuthMiddleware implements MiddlewareInterface
{
    public function __construct(
        private AuthService $authService
    ) {
    }

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Extract token from Authorization header
        $token = $this->extractBearerToken($request);
        
        if ($token === null) {
            return response()->unauthorized()->json([
                'error' => 'Authentication required',
                'message' => 'No authentication token provided'
            ]);
        }
        
        // Validate token
        try {
            $user = $this->authService->validateToken($token);
        } catch (\Exception $e) {
            return response()->unauthorized()->json([
                'error' => 'Invalid token',
                'message' => $e->getMessage()
            ]);
        }
        
        // Add user to request attributes
        $request = $request->withAttribute('user', $user);
        $request = $request->withAttribute('user_id', $user['id']);
        
        return $handler->handle($request);
    }

    private function extractBearerToken(ServerRequestInterface $request): ?string
    {
        $header = $request->getHeaderLine('Authorization');
        
        if (preg_match('/Bearer\s+(.*)$/i', $header, $matches)) {
            return $matches[1];
        }
        
        return null;
    }
}

Auth Service Example

php
namespace ElliePHP\Framework\Application\Services;

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

final readonly class AuthService
{
    private string $jwtSecret;
    private string $jwtAlgorithm;

    public function __construct()
    {
        $this->jwtSecret = env('JWT_SECRET');
        $this->jwtAlgorithm = env('JWT_ALGORITHM', 'HS256');
    }

    public function validateToken(string $token): array
    {
        try {
            $decoded = JWT::decode($token, new Key($this->jwtSecret, $this->jwtAlgorithm));
            
            return [
                'id' => $decoded->user_id,
                'email' => $decoded->email,
                'roles' => $decoded->roles ?? [],
            ];
        } catch (\Exception $e) {
            throw new \RuntimeException('Invalid or expired token');
        }
    }

    public function generateToken(array $user): string
    {
        $payload = [
            'user_id' => $user['id'],
            'email' => $user['email'],
            'roles' => $user['roles'] ?? [],
            'iat' => time(),
            'exp' => time() + (int) env('JWT_TTL', 3600),
        ];
        
        return JWT::encode($payload, $this->jwtSecret, $this->jwtAlgorithm);
    }
}

3. Authorization Strategies

Control access to resources based on user permissions.

Role-Based Authorization Middleware

php
namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final readonly class RoleMiddleware implements MiddlewareInterface
{
    public function __construct(
        private array $requiredRoles
    ) {
    }

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $user = $request->getAttribute('user');
        
        if ($user === null) {
            return response()->unauthorized()->json([
                'error' => 'Authentication required'
            ]);
        }
        
        $userRoles = $user['roles'] ?? [];
        
        // Check if user has any of the required roles
        $hasRole = !empty(array_intersect($this->requiredRoles, $userRoles));
        
        if (!$hasRole) {
            return response()->forbidden()->json([
                'error' => 'Insufficient permissions',
                'required_roles' => $this->requiredRoles
            ]);
        }
        
        return $handler->handle($request);
    }
}

Permission-Based Authorization

php
namespace ElliePHP\Framework\Application\Services;

final readonly class AuthorizationService
{
    public function can(array $user, string $permission): bool
    {
        $permissions = $user['permissions'] ?? [];
        
        return in_array($permission, $permissions);
    }

    public function canAny(array $user, array $permissions): bool
    {
        $userPermissions = $user['permissions'] ?? [];
        
        return !empty(array_intersect($permissions, $userPermissions));
    }

    public function canAll(array $user, array $permissions): bool
    {
        $userPermissions = $user['permissions'] ?? [];
        
        return empty(array_diff($permissions, $userPermissions));
    }
}

Using Authorization in Controllers

php
final readonly class PostController
{
    public function __construct(
        private AuthorizationService $auth
    ) {
    }

    public function update(ServerRequestInterface $request, int $id): ResponseInterface
    {
        $user = $request->getAttribute('user');
        
        if (!$this->auth->can($user, 'posts.update')) {
            return response()->forbidden()->json([
                'error' => 'You do not have permission to update posts'
            ]);
        }
        
        // Update post...
    }
}

4. Input Validation and Sanitization

Always validate and sanitize user input to prevent security vulnerabilities.

Input Validation Example

php
namespace ElliePHP\Framework\Application\Services;

final class ValidationService
{
    public function validateUserInput(array $data): array
    {
        $errors = [];
        
        // Email validation
        if (empty($data['email'])) {
            $errors['email'][] = 'Email is required';
        } elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
            $errors['email'][] = 'Email must be valid';
        }
        
        // Password validation
        if (empty($data['password'])) {
            $errors['password'][] = 'Password is required';
        } elseif (strlen($data['password']) < 8) {
            $errors['password'][] = 'Password must be at least 8 characters';
        }
        
        // Name sanitization and validation
        if (empty($data['name'])) {
            $errors['name'][] = 'Name is required';
        } else {
            $data['name'] = htmlspecialchars(trim($data['name']), ENT_QUOTES, 'UTF-8');
        }
        
        return $errors;
    }
}

Input Sanitization Helpers

php
function sanitizeString(string $input): string
{
    return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}

function sanitizeEmail(string $email): string
{
    return filter_var(trim($email), FILTER_SANITIZE_EMAIL);
}

function sanitizeInt(mixed $value): int
{
    return (int) filter_var($value, FILTER_SANITIZE_NUMBER_INT);
}

function sanitizeUrl(string $url): string
{
    return filter_var($url, FILTER_SANITIZE_URL);
}

5. Password Security

Use the Hash utility class for secure password handling.

Password Hashing

php
use ElliePHP\Components\Support\Util\Hash;

// Hash password
$hashedPassword = Hash::create($plainPassword);

// Verify password
$isValid = Hash::check($plainPassword, $hashedPassword);

Complete Authentication Example

php
namespace ElliePHP\Framework\Application\Services;

use ElliePHP\Components\Support\Util\Hash;

final readonly class UserAuthService
{
    public function __construct(
        private UserRepository $repository
    ) {
    }

    public function register(array $data): array
    {
        // Hash password before storing
        $data['password'] = Hash::create($data['password']);
        
        return $this->repository->create($data);
    }

    public function login(string $email, string $password): ?array
    {
        $user = $this->repository->findByEmail($email);
        
        if ($user === null) {
            return null;
        }
        
        // Verify password
        if (!Hash::check($password, $user['password'])) {
            return null;
        }
        
        // Remove password from returned data
        unset($user['password']);
        
        return $user;
    }
}

Password Requirements

Enforce strong password requirements:

php
function validatePassword(string $password): array
{
    $errors = [];
    
    if (strlen($password) < 8) {
        $errors[] = 'Password must be at least 8 characters';
    }
    
    if (!preg_match('/[A-Z]/', $password)) {
        $errors[] = 'Password must contain at least one uppercase letter';
    }
    
    if (!preg_match('/[a-z]/', $password)) {
        $errors[] = 'Password must contain at least one lowercase letter';
    }
    
    if (!preg_match('/[0-9]/', $password)) {
        $errors[] = 'Password must contain at least one number';
    }
    
    if (!preg_match('/[^A-Za-z0-9]/', $password)) {
        $errors[] = 'Password must contain at least one special character';
    }
    
    return $errors;
}

6. SQL Injection Prevention

Always use prepared statements to prevent SQL injection.

✅ Secure: Using Prepared Statements

php
// Good: Prepared statement with parameter binding
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);

// Good: Named parameters
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status = :status');
$stmt->execute(['email' => $email, 'status' => $status]);

❌ Insecure: String Concatenation

php
// Bad: Direct string concatenation (NEVER DO THIS)
$query = "SELECT * FROM users WHERE email = '$email'";
$result = $pdo->query($query);

// Bad: String interpolation (NEVER DO THIS)
$query = "SELECT * FROM users WHERE email = '{$email}'";
$result = $pdo->query($query);

Repository Pattern with Prepared Statements

php
final readonly class UserRepository
{
    public function findByEmail(string $email): ?array
    {
        $stmt = $this->database->prepare('SELECT * FROM users WHERE email = ?');
        $stmt->execute([$email]);
        
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        return $user ?: null;
    }

    public function search(array $filters): array
    {
        $query = 'SELECT * FROM users WHERE 1=1';
        $params = [];
        
        if (!empty($filters['email'])) {
            $query .= ' AND email LIKE ?';
            $params[] = '%' . $filters['email'] . '%';
        }
        
        if (!empty($filters['status'])) {
            $query .= ' AND status = ?';
            $params[] = $filters['status'];
        }
        
        $stmt = $this->database->prepare($query);
        $stmt->execute($params);
        
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

7. XSS Prevention

Prevent Cross-Site Scripting by escaping output.

Escape Output

php
// Escape HTML output
$safeName = htmlspecialchars($user['name'], ENT_QUOTES, 'UTF-8');

// Escape for JavaScript
$safeJson = json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);

Content Security Policy

Add CSP headers to prevent XSS:

php
return response()
    ->json($data)
    ->withHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'");

8. CSRF Protection

Implement CSRF tokens for state-changing operations.

CSRF Middleware Example

php
namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\SimpleCache\CacheInterface;

final readonly class CsrfMiddleware implements MiddlewareInterface
{
    public function __construct(
        private CacheInterface $cache
    ) {
    }

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Skip CSRF check for safe methods
        if (in_array($request->getMethod(), ['GET', 'HEAD', 'OPTIONS'])) {
            return $handler->handle($request);
        }
        
        // Get token from request
        $token = $request->getHeaderLine('X-CSRF-Token') 
            ?: ($request->getParsedBody()['_csrf_token'] ?? null);
        
        if ($token === null) {
            return response()->forbidden()->json([
                'error' => 'CSRF token missing'
            ]);
        }
        
        // Validate token
        $storedToken = $this->cache->get("csrf_token:{$token}");
        
        if ($storedToken === null) {
            return response()->forbidden()->json([
                'error' => 'Invalid or expired CSRF token'
            ]);
        }
        
        // Delete token after use (one-time use)
        $this->cache->delete("csrf_token:{$token}");
        
        return $handler->handle($request);
    }
}

9. Rate Limiting

Prevent abuse with rate limiting.

Rate Limiting Middleware

php
namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\SimpleCache\CacheInterface;

final readonly class RateLimitMiddleware implements MiddlewareInterface
{
    public function __construct(
        private CacheInterface $cache,
        private int $maxRequests = 60,
        private int $windowSeconds = 60
    ) {
    }

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $clientIp = $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown';
        $cacheKey = "rate_limit:{$clientIp}";
        
        $requests = (int) $this->cache->get($cacheKey, 0);
        
        if ($requests >= $this->maxRequests) {
            return response()->tooManyRequests()->json([
                'error' => 'Rate limit exceeded',
                'retry_after' => $this->windowSeconds
            ])->withHeader('Retry-After', (string) $this->windowSeconds);
        }
        
        $this->cache->set($cacheKey, $requests + 1, $this->windowSeconds);
        
        $response = $handler->handle($request);
        
        return $response
            ->withHeader('X-RateLimit-Limit', (string) $this->maxRequests)
            ->withHeader('X-RateLimit-Remaining', (string) ($this->maxRequests - $requests - 1));
    }
}

10. HTTPS/TLS Configuration

Always use HTTPS in production.

Force HTTPS Middleware

php
namespace ElliePHP\Framework\Application\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class ForceHttpsMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Check if request is not HTTPS
        if ($request->getUri()->getScheme() !== 'https') {
            $httpsUri = $request->getUri()->withScheme('https');
            
            return response()->redirectPermanent((string) $httpsUri);
        }
        
        $response = $handler->handle($request);
        
        // Add HSTS header
        return $response->withHeader(
            'Strict-Transport-Security',
            'max-age=31536000; includeSubDomains'
        );
    }
}

Security Checklist

Use this checklist to ensure your application is secure:

Authentication & Authorization

  • [ ] Implement authentication middleware
  • [ ] Use JWT or session-based authentication
  • [ ] Implement role-based or permission-based authorization
  • [ ] Protect sensitive endpoints with auth middleware

Input Validation

  • [ ] Validate all user input
  • [ ] Sanitize input before processing
  • [ ] Use type declarations and strict types
  • [ ] Implement request validation

Password Security

  • [ ] Use Hash::create() for password hashing
  • [ ] Never store plain text passwords
  • [ ] Enforce strong password requirements
  • [ ] Implement password reset functionality securely

Database Security

  • [ ] Always use prepared statements
  • [ ] Never concatenate SQL queries
  • [ ] Use parameterized queries
  • [ ] Implement proper error handling

API Security

  • [ ] Configure CORS properly (don't use *)
  • [ ] Implement rate limiting
  • [ ] Use HTTPS/TLS in production
  • [ ] Add security headers

Error Handling

  • [ ] Disable debug mode in production
  • [ ] Don't expose sensitive information in errors
  • [ ] Log errors securely
  • [ ] Return generic error messages to users

Environment

  • [ ] Keep .env file secure (not in version control)
  • [ ] Use strong JWT secrets
  • [ ] Rotate secrets regularly
  • [ ] Use environment-specific configurations

Dependencies

  • [ ] Keep dependencies updated
  • [ ] Use roave/security-advisories
  • [ ] Review third-party packages
  • [ ] Monitor security advisories

See Also:

12.7 Testing Strategies

Testing is essential for building reliable applications. ElliePHP applications can be tested using PHPUnit, the de facto standard for PHP testing. This section covers testing strategies for controllers, services, repositories, and other components.

Overview of Testing

ElliePHP supports multiple testing approaches:

  1. Unit Testing - Test individual classes and methods in isolation
  2. Integration Testing - Test how components work together
  3. HTTP Testing - Test controllers and HTTP responses
  4. Repository Testing - Test data access layer
  5. Service Testing - Test business logic
  6. Middleware Testing - Test request/response processing

Setting Up PHPUnit

ElliePHP includes PHPUnit as a development dependency.

PHPUnit Configuration

Create phpunit.xml in your project root:

xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         failOnWarning="true"
         failOnRisky="true"
         beStrictAboutOutputDuringTests="true"
         cacheDirectory=".phpunit.cache">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="Integration">
            <directory>tests/Integration</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory>tests/Feature</directory>
        </testsuite>
    </testsuites>
    <source>
        <include>
            <directory>src</directory>
            <directory>app</directory>
        </include>
    </source>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="APP_DEBUG" value="true"/>
        <env name="CACHE_DRIVER" value="file"/>
    </php>
</phpunit>

Test Directory Structure

tests/
├── Unit/              # Unit tests for individual classes
│   ├── Services/
│   ├── Repositories/
│   └── Utilities/
├── Integration/       # Integration tests
│   └── Database/
└── Feature/          # Feature/HTTP tests
    └── Controllers/

Running Tests

bash
# Run all tests
composer test

# Or use PHPUnit directly
vendor/bin/phpunit

# Run specific test suite
vendor/bin/phpunit --testsuite=Unit

# Run specific test file
vendor/bin/phpunit tests/Unit/Services/UserServiceTest.php

# Run with coverage
composer test:coverage

Unit Testing Services

Test business logic in service classes.

Example: UserService Test

php
namespace ElliePHP\Framework\Tests\Unit\Services;

use ElliePHP\Framework\Application\Services\UserService;
use ElliePHP\Framework\Application\Repositories\UserRepository;
use PHPUnit\Framework\TestCase;
use Psr\SimpleCache\CacheInterface;

final class UserServiceTest extends TestCase
{
    private UserService $service;
    private UserRepository $repository;
    private CacheInterface $cache;

    protected function setUp(): void
    {
        // Create mocks
        $this->repository = $this->createMock(UserRepository::class);
        $this->cache = $this->createMock(CacheInterface::class);
        
        // Create service with mocked dependencies
        $this->service = new UserService($this->repository, $this->cache);
    }

    public function testGetAllUsersReturnsUsersFromCache(): void
    {
        $expectedUsers = [
            ['id' => 1, 'name' => 'John Doe'],
            ['id' => 2, 'name' => 'Jane Smith'],
        ];
        
        // Mock cache to return users
        $this->cache
            ->expects($this->once())
            ->method('get')
            ->with('users')
            ->willReturn($expectedUsers);
        
        // Repository should not be called when cache hits
        $this->repository
            ->expects($this->never())
            ->method('findAll');
        
        $result = $this->service->getAllUsers();
        
        $this->assertEquals($expectedUsers, $result);
    }

    public function testGetAllUsersReturnsUsersFromRepositoryWhenCacheMisses(): void
    {
        $expectedUsers = [
            ['id' => 1, 'name' => 'John Doe'],
        ];
        
        // Mock cache miss
        $this->cache
            ->expects($this->once())
            ->method('get')
            ->with('users')
            ->willReturn(null);
        
        // Mock repository to return users
        $this->repository
            ->expects($this->once())
            ->method('findAll')
            ->willReturn($expectedUsers);
        
        // Cache should be set
        $this->cache
            ->expects($this->once())
            ->method('set')
            ->with('users', $expectedUsers, 3600);
        
        $result = $this->service->getAllUsers();
        
        $this->assertEquals($expectedUsers, $result);
    }

    public function testGetUserByIdReturnsUser(): void
    {
        $userId = 1;
        $expectedUser = ['id' => 1, 'name' => 'John Doe'];
        
        $this->cache
            ->method('get')
            ->willReturn(null);
        
        $this->repository
            ->expects($this->once())
            ->method('findById')
            ->with($userId)
            ->willReturn($expectedUser);
        
        $result = $this->service->getUserById($userId);
        
        $this->assertEquals($expectedUser, $result);
    }

    public function testGetUserByIdReturnsNullWhenUserNotFound(): void
    {
        $userId = 999;
        
        $this->cache
            ->method('get')
            ->willReturn(null);
        
        $this->repository
            ->expects($this->once())
            ->method('findById')
            ->with($userId)
            ->willReturn(null);
        
        $result = $this->service->getUserById($userId);
        
        $this->assertNull($result);
    }

    public function testCreateUserValidatesData(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Valid email is required');
        
        $invalidData = [
            'name' => 'John Doe',
            'email' => 'invalid-email', // Invalid email
        ];
        
        $this->service->createUser($invalidData);
    }

    public function testCreateUserCreatesAndClearsCache(): void
    {
        $userData = [
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'password123',
        ];
        
        $createdUser = array_merge($userData, ['id' => 1]);
        
        $this->repository
            ->expects($this->once())
            ->method('create')
            ->with($userData)
            ->willReturn($createdUser);
        
        // Cache should be cleared
        $this->cache
            ->expects($this->once())
            ->method('delete')
            ->with('users');
        
        $result = $this->service->createUser($userData);
        
        $this->assertEquals($createdUser, $result);
    }
}

Testing Repositories

Test data access layer with a test database.

Example: UserRepository Test

php
namespace ElliePHP\Framework\Tests\Integration\Repositories;

use ElliePHP\Framework\Application\Repositories\UserRepository;
use PHPUnit\Framework\TestCase;
use PDO;

final class UserRepositoryTest extends TestCase
{
    private PDO $database;
    private UserRepository $repository;

    protected function setUp(): void
    {
        // Create in-memory SQLite database for testing
        $this->database = new PDO('sqlite::memory:');
        $this->database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        
        // Create users table
        $this->database->exec('
            CREATE TABLE users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                email TEXT UNIQUE NOT NULL,
                password TEXT NOT NULL,
                created_at TEXT NOT NULL,
                updated_at TEXT
            )
        ');
        
        $this->repository = new UserRepository($this->database);
    }

    protected function tearDown(): void
    {
        // Clean up
        $this->database = null;
    }

    public function testFindAllReturnsEmptyArrayWhenNoUsers(): void
    {
        $result = $this->repository->findAll();
        
        $this->assertIsArray($result);
        $this->assertEmpty($result);
    }

    public function testFindAllReturnsAllUsers(): void
    {
        // Insert test data
        $this->database->exec("
            INSERT INTO users (name, email, password, created_at) VALUES
            ('John Doe', 'john@example.com', 'hashed1', '2024-01-01 00:00:00'),
            ('Jane Smith', 'jane@example.com', 'hashed2', '2024-01-02 00:00:00')
        ");
        
        $result = $this->repository->findAll();
        
        $this->assertCount(2, $result);
        $this->assertEquals('John Doe', $result[0]['name']);
        $this->assertEquals('Jane Smith', $result[1]['name']);
    }

    public function testFindByIdReturnsUserWhenExists(): void
    {
        $this->database->exec("
            INSERT INTO users (name, email, password, created_at) 
            VALUES ('John Doe', 'john@example.com', 'hashed', '2024-01-01 00:00:00')
        ");
        
        $user = $this->repository->findById(1);
        
        $this->assertNotNull($user);
        $this->assertEquals('John Doe', $user['name']);
        $this->assertEquals('john@example.com', $user['email']);
    }

    public function testFindByIdReturnsNullWhenUserNotExists(): void
    {
        $user = $this->repository->findById(999);
        
        $this->assertNull($user);
    }

    public function testCreateInsertsUserAndReturnsData(): void
    {
        $userData = [
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'hashed_password',
        ];
        
        $user = $this->repository->create($userData);
        
        $this->assertIsArray($user);
        $this->assertArrayHasKey('id', $user);
        $this->assertEquals('John Doe', $user['name']);
        $this->assertEquals('john@example.com', $user['email']);
    }

    public function testUpdateModifiesUserData(): void
    {
        // Create user
        $user = $this->repository->create([
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'hashed',
        ]);
        
        // Update user
        $updated = $this->repository->update($user['id'], [
            'name' => 'John Updated',
            'email' => 'john.updated@example.com',
        ]);
        
        $this->assertEquals('John Updated', $updated['name']);
        $this->assertEquals('john.updated@example.com', $updated['email']);
    }

    public function testDeleteRemovesUser(): void
    {
        // Create user
        $user = $this->repository->create([
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'hashed',
        ]);
        
        // Delete user
        $result = $this->repository->delete($user['id']);
        
        $this->assertTrue($result);
        
        // Verify user is deleted
        $deletedUser = $this->repository->findById($user['id']);
        $this->assertNull($deletedUser);
    }

    public function testEmailExistsReturnsTrueWhenEmailExists(): void
    {
        $this->repository->create([
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'hashed',
        ]);
        
        $exists = $this->repository->emailExists('john@example.com');
        
        $this->assertTrue($exists);
    }

    public function testEmailExistsReturnsFalseWhenEmailNotExists(): void
    {
        $exists = $this->repository->emailExists('nonexistent@example.com');
        
        $this->assertFalse($exists);
    }
}

Testing Controllers

Test HTTP controllers and responses.

Example: UserController Test

php
namespace ElliePHP\Framework\Tests\Feature\Controllers;

use ElliePHP\Framework\Application\Http\Controllers\UserController;
use ElliePHP\Framework\Application\Services\UserService;
use Laminas\Diactoros\ServerRequest;
use PHPUnit\Framework\TestCase;

final class UserControllerTest extends TestCase
{
    private UserController $controller;
    private UserService $userService;

    protected function setUp(): void
    {
        $this->userService = $this->createMock(UserService::class);
        $this->controller = new UserController($this->userService);
    }

    public function testIndexReturnsJsonResponseWithUsers(): void
    {
        $expectedUsers = [
            ['id' => 1, 'name' => 'John Doe'],
            ['id' => 2, 'name' => 'Jane Smith'],
        ];
        
        $this->userService
            ->expects($this->once())
            ->method('getAllUsers')
            ->willReturn($expectedUsers);
        
        $response = $this->controller->index();
        
        $this->assertEquals(200, $response->getStatusCode());
        $this->assertEquals('application/json', $response->getHeaderLine('Content-Type'));
        
        $body = json_decode((string) $response->getBody(), true);
        $this->assertArrayHasKey('users', $body);
        $this->assertEquals($expectedUsers, $body['users']);
    }

    public function testShowReturnsUserWhenExists(): void
    {
        $userId = 1;
        $expectedUser = ['id' => 1, 'name' => 'John Doe'];
        
        $this->userService
            ->expects($this->once())
            ->method('getUserById')
            ->with($userId)
            ->willReturn($expectedUser);
        
        $response = $this->controller->show($userId);
        
        $this->assertEquals(200, $response->getStatusCode());
        
        $body = json_decode((string) $response->getBody(), true);
        $this->assertArrayHasKey('user', $body);
        $this->assertEquals($expectedUser, $body['user']);
    }

    public function testShowReturns404WhenUserNotFound(): void
    {
        $userId = 999;
        
        $this->userService
            ->expects($this->once())
            ->method('getUserById')
            ->with($userId)
            ->willReturn(null);
        
        $response = $this->controller->show($userId);
        
        $this->assertEquals(404, $response->getStatusCode());
        
        $body = json_decode((string) $response->getBody(), true);
        $this->assertArrayHasKey('error', $body);
        $this->assertEquals('User not found', $body['error']);
    }

    public function testStoreCreatesUserAndReturns201(): void
    {
        $userData = [
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'password123',
        ];
        
        $createdUser = array_merge($userData, ['id' => 1]);
        unset($createdUser['password']);
        
        $this->userService
            ->expects($this->once())
            ->method('createUser')
            ->with($userData)
            ->willReturn($createdUser);
        
        $request = new ServerRequest();
        $request = $request->withParsedBody($userData);
        
        $response = $this->controller->store($request);
        
        $this->assertEquals(201, $response->getStatusCode());
        
        $body = json_decode((string) $response->getBody(), true);
        $this->assertArrayHasKey('user', $body);
        $this->assertEquals($createdUser, $body['user']);
    }

    public function testStoreReturns400OnValidationError(): void
    {
        $invalidData = [
            'name' => 'John Doe',
            'email' => 'invalid-email',
        ];
        
        $this->userService
            ->expects($this->once())
            ->method('createUser')
            ->with($invalidData)
            ->willThrowException(new \InvalidArgumentException('Valid email is required'));
        
        $request = new ServerRequest();
        $request = $request->withParsedBody($invalidData);
        
        $response = $this->controller->store($request);
        
        $this->assertEquals(400, $response->getStatusCode());
        
        $body = json_decode((string) $response->getBody(), true);
        $this->assertArrayHasKey('error', $body);
    }
}

Testing Middleware

Test middleware request/response processing.

Example: AuthMiddleware Test

php
namespace ElliePHP\Framework\Tests\Unit\Middlewares;

use ElliePHP\Framework\Application\Http\Middlewares\AuthMiddleware;
use ElliePHP\Framework\Application\Services\AuthService;
use Laminas\Diactoros\Response\JsonResponse;
use Laminas\Diactoros\ServerRequest;
use PHPUnit\Framework\TestCase;
use Psr\Http\Server\RequestHandlerInterface;

final class AuthMiddlewareTest extends TestCase
{
    private AuthMiddleware $middleware;
    private AuthService $authService;
    private RequestHandlerInterface $handler;

    protected function setUp(): void
    {
        $this->authService = $this->createMock(AuthService::class);
        $this->handler = $this->createMock(RequestHandlerInterface::class);
        $this->middleware = new AuthMiddleware($this->authService);
    }

    public function testReturns401WhenNoTokenProvided(): void
    {
        $request = new ServerRequest();
        
        $response = $this->middleware->process($request, $this->handler);
        
        $this->assertEquals(401, $response->getStatusCode());
        
        $body = json_decode((string) $response->getBody(), true);
        $this->assertArrayHasKey('error', $body);
        $this->assertEquals('Authentication required', $body['error']);
    }

    public function testReturns401WhenTokenIsInvalid(): void
    {
        $request = new ServerRequest();
        $request = $request->withHeader('Authorization', 'Bearer invalid-token');
        
        $this->authService
            ->expects($this->once())
            ->method('validateToken')
            ->with('invalid-token')
            ->willThrowException(new \RuntimeException('Invalid token'));
        
        $response = $this->middleware->process($request, $this->handler);
        
        $this->assertEquals(401, $response->getStatusCode());
    }

    public function testAddsUserToRequestWhenTokenIsValid(): void
    {
        $token = 'valid-token';
        $user = ['id' => 1, 'email' => 'john@example.com'];
        
        $request = new ServerRequest();
        $request = $request->withHeader('Authorization', "Bearer {$token}");
        
        $this->authService
            ->expects($this->once())
            ->method('validateToken')
            ->with($token)
            ->willReturn($user);
        
        $this->handler
            ->expects($this->once())
            ->method('handle')
            ->with($this->callback(function ($req) use ($user) {
                return $req->getAttribute('user') === $user;
            }))
            ->willReturn(new JsonResponse(['success' => true]));
        
        $response = $this->middleware->process($request, $this->handler);
        
        $this->assertEquals(200, $response->getStatusCode());
    }
}

Container Mocking for Tests

Mock the dependency injection container for testing.

Example: Container Mock Helper

php
namespace ElliePHP\Framework\Tests;

use ElliePHP\Components\Framework\Support\Container;
use PHPUnit\Framework\TestCase;

abstract class TestCase extends \PHPUnit\Framework\TestCase
{
    protected function mockContainer(string $abstract, object $mock): void
    {
        $container = Container::getInstance();
        $container->set($abstract, $mock);
    }

    protected function tearDown(): void
    {
        // Reset container after each test
        Container::getInstance()->flush();
        parent::tearDown();
    }
}

Using Container Mocks:

php
final class SomeFeatureTest extends TestCase
{
    public function testFeatureWithMockedService(): void
    {
        $mockService = $this->createMock(UserService::class);
        $mockService->method('getAllUsers')->willReturn([]);
        
        $this->mockContainer(UserService::class, $mockService);
        
        // Test code that uses container(UserService::class)
    }
}

Test Data Factories

Create test data factories for consistent test data.

Example: User Factory

php
namespace ElliePHP\Framework\Tests\Factories;

final class UserFactory
{
    public static function make(array $attributes = []): array
    {
        return array_merge([
            'id' => 1,
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'created_at' => '2024-01-01 00:00:00',
        ], $attributes);
    }

    public static function makeMany(int $count, array $attributes = []): array
    {
        $users = [];
        
        for ($i = 1; $i <= $count; $i++) {
            $users[] = self::make(array_merge($attributes, ['id' => $i]));
        }
        
        return $users;
    }
}

Using Factories:

php
public function testGetAllUsers(): void
{
    $users = UserFactory::makeMany(3);
    
    $this->repository
        ->method('findAll')
        ->willReturn($users);
    
    $result = $this->service->getAllUsers();
    
    $this->assertCount(3, $result);
}

Testing Best Practices

1. Follow AAA Pattern

Arrange, Act, Assert:

php
public function testExample(): void
{
    // Arrange: Set up test data and mocks
    $user = UserFactory::make();
    $this->repository->method('findById')->willReturn($user);
    
    // Act: Execute the code being tested
    $result = $this->service->getUserById(1);
    
    // Assert: Verify the results
    $this->assertEquals($user, $result);
}

2. Test One Thing Per Test

php
// Good: Tests one specific behavior
public function testGetUserByIdReturnsUser(): void { }
public function testGetUserByIdReturnsNullWhenNotFound(): void { }

// Bad: Tests multiple things
public function testGetUserById(): void {
    // Tests both success and failure cases
}

3. Use Descriptive Test Names

php
// Good: Describes what is being tested and expected outcome
public function testCreateUserThrowsExceptionWhenEmailIsInvalid(): void { }

// Bad: Vague test name
public function testCreateUser(): void { }

4. Don't Test Framework Code

Test your code, not the framework:

php
// Good: Tests your business logic
public function testUserServiceValidatesEmail(): void { }

// Bad: Tests framework functionality
public function testResponseReturnsJson(): void { }

5. Use Mocks Appropriately

Mock external dependencies, not the class under test:

php
// Good: Mock dependencies
$mockRepository = $this->createMock(UserRepository::class);
$service = new UserService($mockRepository);

// Bad: Mock the class being tested
$mockService = $this->createMock(UserService::class);

6. Keep Tests Fast

  • Use in-memory databases for repository tests
  • Mock external API calls
  • Avoid unnecessary setup
  • Run unit tests more frequently than integration tests

7. Test Edge Cases

php
public function testGetUserByIdWithNegativeId(): void { }
public function testGetUserByIdWithZeroId(): void { }
public function testGetUserByIdWithVeryLargeId(): void { }

8. Use Data Providers for Similar Tests

php
/**
 * @dataProvider invalidEmailProvider
 */
public function testCreateUserThrowsExceptionForInvalidEmail(string $email): void
{
    $this->expectException(\InvalidArgumentException::class);
    $this->service->createUser(['email' => $email]);
}

public static function invalidEmailProvider(): array
{
    return [
        ['invalid'],
        ['@example.com'],
        ['user@'],
        [''],
    ];
}

Composer Test Scripts

Add test scripts to composer.json:

json
{
    "scripts": {
        "test": "phpunit",
        "test:unit": "phpunit --testsuite=Unit",
        "test:integration": "phpunit --testsuite=Integration",
        "test:feature": "phpunit --testsuite=Feature",
        "test:coverage": "phpunit --coverage-html coverage",
        "test:coverage-text": "phpunit --coverage-text"
    }
}

Run Tests:

bash
composer test              # All tests
composer test:unit         # Unit tests only
composer test:integration  # Integration tests only
composer test:coverage     # Generate HTML coverage report

Continuous Integration

Set up CI/CD to run tests automatically.

Example: GitHub Actions

yaml
# .github/workflows/tests.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          extensions: pdo, json, mbstring
          coverage: xdebug
      
      - name: Install dependencies
        run: composer install --prefer-dist --no-progress
      
      - name: Run tests
        run: composer test
      
      - name: Generate coverage
        run: composer test:coverage-text

See Also:


13. API Reference

13.1 Helper Functions Quick Reference

This section provides a quick reference for all global helper functions available in ElliePHP. These functions provide convenient access to framework services and utilities.

cache()

Get a Cache instance with the specified driver.

php
function cache(?string $cacheDriver = null): Cache

Parameters:

  • $cacheDriver (string|null): Cache driver name ('file', 'redis', 'sqlite', 'apcu'). Uses CACHE_DRIVER env if null.

Returns: Cache instance with configured driver

Example:

php
cache()->set('key', 'value', 3600);
$value = cache()->get('key');
cache('redis')->set('key', 'value');

See Also: Caching


config()

Get or set configuration values using dot notation.

php
function config(array|string|null $key = null, mixed $default = null): mixed

Parameters:

  • $key (array|string|null): Configuration key or array of key-value pairs to set. Returns ConfigParser if null.
  • $default (mixed): Default value if key doesn't exist

Returns: Configuration value, ConfigParser instance, or null

Example:

php
$appName = config('app.name');
config('app.debug', false);
config(['app.name' => 'MyApp']);

See Also: Configuration


container()

Get the container instance or resolve a service.

php
function container(?string $abstract = null): mixed

Parameters:

  • $abstract (string|null): Service identifier to resolve. Returns container if null.

Returns: Container instance or resolved service

Throws: ContainerExceptionInterface, NotFoundExceptionInterface

Example:

php
$container = container();
$service = container(UserService::class);

See Also: Dependency Injection


env()

Get environment variable value or Env instance.

php
function env(?string $value = null, mixed $defaultValue = null): mixed

Parameters:

  • $value (string|null): Environment variable name. Returns Env instance if null.
  • $defaultValue (mixed): Default value if variable doesn't exist

Returns: Environment value with automatic type casting, or Env instance

Example:

php
$debug = env('APP_DEBUG', false);
$host = env('DB_HOST', 'localhost');
$env = env(); // Get Env instance

See Also: Environment Utilities


report()

Get the Log instance for application logging.

php
function report(): Log

Returns: Logger instance with app and exception channels

Example:

php
report()->info('User logged in', ['user_id' => 123]);
report()->error('Database connection failed');
report()->exception($exception);

See Also: Logging


request()

Get the current HTTP request instance.

php
function request(): Request

Returns: PSR-7 ServerRequest instance

Example:

php
$email = request()->input('email');
$method = request()->method();
$isJson = request()->isJson();

See Also: HTTP Request & Response


response()

Create a new HTTP response instance.

php
function response(int $status = 200): Response

Parameters:

  • $status (int): HTTP status code (default: 200)

Returns: PSR-7 Response instance

Example:

php
return response()->json(['data' => $users]);
return response(201)->json(['created' => true]);
return response()->redirect('/home');

See Also: HTTP Request & Response


Path Helper Functions

app_path()

Get the application path.

php
function app_path(string $path = ""): string

Parameters:

  • $path (string): Optional path to append

Returns: Full path to app directory

Example:

php
$controllersPath = app_path('Http/Controllers');

root_path()

Get the root path of the application.

php
function root_path(string $path = ""): string

Parameters:

  • $path (string): Optional path to append

Returns: Full path to root directory

Example:

php
$envPath = root_path('.env');

routes_path()

Get the routes path.

php
function routes_path(string $path = ""): string

Parameters:

  • $path (string): Optional path to append

Returns: Full path to routes directory

Example:

php
$routerFile = routes_path('router.php');

storage_path()

Get the storage path.

php
function storage_path(string $path = ""): string

Parameters:

  • $path (string): Optional path to append

Returns: Full path to storage directory

Example:

php
$uploadsPath = storage_path('uploads');

storage_cache_path()

Get the storage cache path.

php
function storage_cache_path(string $path = ""): string

Parameters:

  • $path (string): Optional path to append

Returns: Full path to storage/Cache directory

Example:

php
$cacheDb = storage_cache_path('cache.db');

storage_logs_path()

Get the storage logs path.

php
function storage_logs_path(string $path = ""): string

Parameters:

  • $path (string): Optional path to append

Returns: Full path to storage/Logs directory

Example:

php
$appLog = storage_logs_path('app.log');

13.2 Utility Classes Quick Reference

This section provides a quick reference for all utility classes available in ElliePHP. These classes provide static methods for common operations.

Str Class

String manipulation and validation utilities.

Case Conversion:

php
Str::toCamelCase(string $string): string
Str::toPascalCase(string $string): string
Str::toSnakeCase(string $string): string
Str::toKebabCase(string $string): string
Str::toUpperCase(string $string): string
Str::toLowerCase(?string $string, ?string $encoding = "UTF-8"): string
Str::title(string $string): string
Str::ucfirst(string $string): string
Str::lcfirst(string $string): string

String Operations:

php
Str::startsWith(string $haystack, string $needle): bool
Str::startsWithAny(string $haystack, array $needles): bool
Str::endsWith(string $haystack, string $needle): bool
Str::endsWithAny(string $haystack, array $needles): bool
Str::contains(string $haystack, string $needle): bool
Str::containsAny(string $haystack, array $needles): bool
Str::containsAll(string $haystack, array $needles): bool
Str::limit(string $string, int $limit = 100, string $end = "..."): string
Str::truncateWords(string $string, int $words = 10, string $end = "..."): string
Str::words(string $string, int $words = 10): string
Str::wordCount(string $string): int
Str::slug(string $string, string $separator = "-"): string
Str::length(string $string): int
Str::reverse(string $string): string

String Manipulation:

php
Str::replace(string $search, string $replace, string $subject): string
Str::replaceFirst(string $search, string $replace, string $subject): string
Str::replaceLast(string $search, string $replace, string $subject): string
Str::replaceArray(array $search, array $replace, string $subject): string
Str::clean(string $string): string
Str::trim(string $string, string $characters = " \t\n\r\0\x0B"): string
Str::ltrim(string $string, string $characters = " \t\n\r\0\x0B"): string
Str::rtrim(string $string, string $characters = " \t\n\r\0\x0B"): string
Str::removePrefix(string $string, string $prefix): string
Str::removeSuffix(string $string, string $suffix): string
Str::ensurePrefix(string $string, string $prefix): string
Str::ensureSuffix(string $string, string $suffix): string

String Extraction:

php
Str::substr(string $string, int $start, ?int $length = null): string
Str::before(string $string, string $search): string
Str::after(string $string, string $search): string
Str::beforeLast(string $string, string $search): string
Str::afterLast(string $string, string $search): string
Str::extractStringBetween(string $string, string $start, string $end): ?string

Padding & Formatting:

php
Str::padLeft(string $string, int $length, string $pad = " "): string
Str::padRight(string $string, int $length, string $pad = " "): string
Str::padBoth(string $string, int $length, string $pad = " "): string
Str::mask(string $string, string $character = "*", int $index = 0, ?int $length = null): string
Str::repeat(string $string, int $times): string

Pattern Matching:

php
Str::match(string $pattern, string $subject): ?array
Str::matchAll(string $pattern, string $subject): ?array

Validation:

php
Str::isEmpty(string $string): bool
Str::isNotEmpty(string $string): bool
Str::isJson(string $string): bool
Str::isUrl(string $string): bool
Str::isEmail(string $string): bool
Str::isAlphanumeric(string $string): bool
Str::isAlpha(string $string): bool
Str::isNumeric(string $string): bool

Utilities:

php
Str::random(int $length = 16): string
Str::swap(string $string, array $replacements): array|string
Str::split(string $seperator, string $string, int $limit = PHP_INT_MAX): array
Str::toArray(string $string): array
Str::cleanUtf8(mixed $value): ?string
Str::plural(string $string, int $count = 2): string
Str::singular(string $string): string

See Also: String Utilities


File Class

File and directory operations.

File Checks:

php
File::exists(string $path): bool
File::isFile(string $path): bool
File::isDirectory(string $path): bool
File::isReadable(string $path): bool
File::isWritable(string $path): bool

File Reading:

php
File::get(string $path): string
File::lines(string $path, bool $skipEmpty = false): array
File::json(string $path, bool $associative = true): mixed

File Writing:

php
File::put(string $path, string $contents, bool $lock = false): int
File::append(string $path, string $contents): int
File::prepend(string $path, string $contents): int
File::putJson(string $path, mixed $data, int $flags = JSON_PRETTY_PRINT): int

File Operations:

php
File::delete(string $path): bool
File::copy(string $source, string $destination): bool
File::move(string $source, string $destination): bool
File::replace(string $path, array|string $search, array|string $replace): int
File::replaceRegex(string $path, array|string $pattern, array|string $replacement): int
File::contains(string $path, string $needle): bool

File Information:

php
File::extension(string $path): string
File::name(string $path): string
File::basename(string $path): string
File::dirname(string $path): string
File::size(string $path): int
File::humanSize(string $path, int $precision = 2): string
File::lastModified(string $path): int
File::mimeType(string $path): ?string
File::permissions(string $path): int
File::chmod(string $path, int $mode): bool
File::hash(string $path, string $algorithm = 'sha256'): string
File::isOlderThan(string $path, int $seconds): bool

Directory Operations:

php
File::files(string $directory, bool $recursive = false): array
File::directories(string $directory): array
File::makeDirectory(string $path, int $mode = 0755, bool $recursive = true): bool
File::deleteDirectory(string $directory, bool $preserve = false): bool
File::cleanDirectory(string $directory): bool
File::copyDirectory(string $source, string $destination): bool
File::moveDirectory(string $source, string $destination): bool

Utilities:

php
File::glob(string $pattern, int $flags = 0): array
File::matchesPattern(string $pattern, string $path): bool
File::relativePath(string $from, string $to): string
File::ensureExists(string $path, string $contents = ''): bool
File::closestExistingDirectory(string $path): string

See Also: File Utilities


Json Class

JSON encoding, decoding, and manipulation.

Encoding & Decoding:

php
Json::encode(mixed $value, int $flags = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE, int $depth = 512): string
Json::decode(string $json, bool $associative = true, int $flags = JSON_THROW_ON_ERROR, int $depth = 512): mixed
Json::pretty(mixed $value, int $flags = 0): string
Json::safeEncode(mixed $value, int $flags = JSON_UNESCAPED_UNICODE, int $depth = 512): ?string
Json::safeDecode(string $json, bool $associative = true, int $depth = 512): mixed

Validation:

php
Json::isValid(string $json): bool
Json::validate(string $json, array $schema): bool
Json::lastError(): string
Json::lastErrorCode(): int

File Operations:

php
Json::fromFile(string $path, bool $associative = true): mixed
Json::toFile(string $path, mixed $value, bool $pretty = false, int $flags = 0): bool

Manipulation:

php
Json::merge(string|array ...$jsons): array
Json::mergeDeep(string|array ...$jsons): array
Json::only(string $json, array $keys): string
Json::except(string $json, array $keys): string
Json::flatten(string $json, string $separator = '.'): string

Dot Notation Access:

php
Json::get(string $json, string $key, mixed $default = null): mixed
Json::set(string $json, string $key, mixed $value): string
Json::has(string $json, string $key): bool
Json::forget(string $json, string $key): string

Formatting:

php
Json::minify(string $json): string
Json::format(string $json): string

Conversion:

php
Json::toXml(string $json, string $rootElement = 'root'): string
Json::toCsv(string $json, bool $includeHeaders = true): string

See Also: JSON Utilities


Hash Class

Hashing, password hashing, and ID generation.

Password Hashing:

php
Hash::create(string $value, array $options = []): string
Hash::check(string $value, string $hash): bool
Hash::needsRehash(string $hash, array $options = []): bool
Hash::info(string $hash): array
Hash::argon2i(string $value, array $options = []): string
Hash::argon2id(string $value, array $options = []): string

Hash Algorithms:

php
Hash::hash(string $value, string $algorithm = 'sha256', bool $binary = false): string
Hash::hmac(string $value, string $key, string $algorithm = 'sha256', bool $binary = false): string
Hash::md5(string $value): string
Hash::sha1(string $value): string
Hash::sha256(string $value): string
Hash::sha512(string $value): string
Hash::xxh3(string $value): string
Hash::crc32(string $value): string

File Hashing:

php
Hash::file(string $path, string $algorithm = 'sha256', bool $binary = false): string|false

ID Generation:

php
Hash::uuid(): string
Hash::ulid(): string
Hash::nanoid(int $size = 21, ?string $alphabet = null): string
Hash::random(int $length = 32): string

Utilities:

php
Hash::equals(string $known, string $user): bool
Hash::base64(string $value, string $algorithm = 'sha256'): string
Hash::base64Url(string $value, string $algorithm = 'sha256'): string
Hash::short(string $value, int $length = 8): string
Hash::salted(string $value, string $salt, string $algorithm = 'sha256'): string
Hash::checksum(string $value, string $algorithm = 'sha256'): string
Hash::verifyChecksum(string $value, string $checksum, string $algorithm = 'sha256'): bool
Hash::algorithms(): array

See Also: Hash Utilities


Env Class

Environment variable management with automatic type casting.

Loading:

php
__construct(string $path, string|array $names = '.env')
load(): self
loadWithRequired(array $required): self
isLoaded(): bool

Access:

php
get(string $key, mixed $default = null): mixed
has(string $key): bool
all(): array

Validation:

php
require(array $variables): self
requireNotEmpty(array $variables): self
requireOneOf(string $variable, array $allowedValues): self

Example:

php
$env = new Env(root_path());
$env->load();
$debug = $env->get('APP_DEBUG', false);
$env->requireNotEmpty(['APP_NAME', 'APP_KEY']);

See Also: Environment Utilities


13.3 HTTP API Quick Reference

This section provides a quick reference for HTTP Request and Response classes.

Request Class

PSR-7 ServerRequest wrapper with convenient methods.

Factory:

php
Request::fromGlobals(): Request

Input Methods:

php
input(string $key, mixed $default = null): mixed
string(string $key, string $default = ''): string
int(string $key, int $default = 0): int
bool(string $key, bool $default = false): bool
float(string $key, float $default = 0.0): float
array(string $key, array $default = []): array

Request Information:

php
method(): string
path(): string
url(): string
fullUrl(): string
isMethod(string $method): bool
isGet(): bool
isPost(): bool
isPut(): bool
isDelete(): bool
isPatch(): bool
isJson(): bool
isXml(): bool
expectsJson(): bool

Headers:

php
header(string $name, mixed $default = null): mixed
headers(): array
hasHeader(string $name): bool
bearerToken(): ?string

Query & Body:

php
query(string $key, mixed $default = null): mixed
all(): array
only(array $keys): array
except(array $keys): array
has(string|array $keys): bool
filled(string $key): bool
missing(string $key): bool

Files:

php
file(string $key): ?UploadedFileInterface
hasFile(string $key): bool
files(): array

Server & Client:

php
server(string $key, mixed $default = null): mixed
ip(): string
userAgent(): ?string

See Also: Request Class


Response Class

PSR-7 Response wrapper with fluent interface.

Factory:

php
Response::make(int $status = 200): Response

Content Type Methods:

php
json(mixed $data, int $status = 200, array $headers = []): ResponseInterface
jsonp(string $callback, mixed $data, int $status = 200): ResponseInterface
html(string $content, int $status = 200): ResponseInterface
text(string $content, int $status = 200): ResponseInterface
xml(string $content, int $status = 200): ResponseInterface

Redirect Methods:

php
redirect(string $url, int $status = 302): ResponseInterface
redirectPermanent(string $url): ResponseInterface
redirectTemporary(string $url): ResponseInterface
redirectSeeOther(string $url): ResponseInterface
back(string $fallback = '/'): ResponseInterface

Status Code Helpers (2xx):

php
ok(mixed $data = null): ResponseInterface
created(mixed $data = null, ?string $location = null): ResponseInterface
accepted(mixed $data = null): ResponseInterface
noContent(): ResponseInterface

Status Code Helpers (4xx):

php
badRequest(mixed $data = null): ResponseInterface
unauthorized(mixed $data = null): ResponseInterface
forbidden(mixed $data = null): ResponseInterface
notFound(mixed $data = null): ResponseInterface
methodNotAllowed(mixed $data = null): ResponseInterface
conflict(mixed $data = null): ResponseInterface
unprocessable(mixed $data = null): ResponseInterface
tooManyRequests(mixed $data = null): ResponseInterface

Status Code Helpers (5xx):

php
serverError(mixed $data = null): ResponseInterface
serviceUnavailable(mixed $data = null): ResponseInterface

File Downloads:

php
download(string $path, ?string $name = null, array $headers = [], bool $deleteAfter = false): ResponseInterface
file(string $path, array $headers = []): ResponseInterface
streamDownload(callable $callback, string $name, array $headers = []): ResponseInterface

Headers & Cookies:

php
withHeader(string $name, string $value): self
withHeaders(array $headers): self
contentType(string $type): self
cacheControl(string $value): self
noCache(): self
etag(string $etag): self
lastModified(int|string $timestamp): self
cookie(string $name, string $value, int $minutes = 0, string $path = '/', ?string $domain = null, bool $secure = false, bool $httpOnly = true): self
withCookie(string $name, string $value, array $options = []): self
withoutCookie(string $name): self

Response Inspection:

php
status(): int
isSuccessful(): bool
isOk(): bool
isRedirect(): bool
isClientError(): bool
isServerError(): bool
body(): string
content(): string
toJson(): string
headers(): array
getHeader(string $name): array
hasHeader(string $name): bool

See Also: Response Class


13.4 Cache API Quick Reference

This section provides a quick reference for the Cache API (PSR-16 Simple Cache).

Cache Class

PSR-16 Simple Cache implementation with multiple drivers.

Basic Operations:

php
get(string $key, mixed $default = null): mixed
set(string $key, mixed $value, null|int|DateInterval $ttl = null): bool
delete(string $key): bool
clear(): bool
has(string $key): bool

Batch Operations:

php
getMultiple(iterable $keys, mixed $default = null): iterable
setMultiple(iterable $values, null|int|DateInterval $ttl = null): bool
deleteMultiple(iterable $keys): bool

Statistics:

php
count(): int
size(): int

Driver-Specific Methods:

php
clearExpired(): bool  // File and SQLite drivers only

Example:

php
// Set cache value
cache()->set('user:123', $user, 3600);

// Get cache value
$user = cache()->get('user:123');

// Check if exists
if (cache()->has('user:123')) {
    // ...
}

// Delete cache value
cache()->delete('user:123');

// Clear all cache
cache()->clear();

// Batch operations
cache()->setMultiple([
    'key1' => 'value1',
    'key2' => 'value2'
], 3600);

$values = cache()->getMultiple(['key1', 'key2']);

See Also: Caching


CacheFactory Class

Factory for creating cache driver instances.

File Driver:

php
CacheFactory::createFileDriver(array $config = []): FileCache

Config Options:

  • path (string): Cache directory path
  • create_directory (bool): Auto-create directory (default: true)
  • directory_permissions (int): Directory permissions (default: 0755)

Redis Driver:

php
CacheFactory::createRedisDriver(array $config = []): RedisCache

Config Options:

  • host (string): Redis host (default: '127.0.0.1')
  • port (int): Redis port (default: 6379)
  • password (string|null): Redis password
  • database (int): Redis database (default: 0)
  • timeout (int): Connection timeout (default: 5)
  • prefix (string): Key prefix (default: 'ellie_cache:')

SQLite Driver:

php
CacheFactory::createSQLiteDriver(array $config = []): SQLiteCache

Config Options:

  • path (string): Database file path
  • create_directory (bool): Auto-create directory (default: true)
  • directory_permissions (int): Directory permissions (default: 0755)

APCu Driver:

php
CacheFactory::createApcuDriver(array $config = []): ApcuCache

Config Options:

  • prefix (string): Key prefix (default: 'ellie_cache:')

Example:

php
// Create file cache
$cache = CacheFactory::createFileDriver([
    'path' => storage_cache_path()
]);

// Create Redis cache
$cache = CacheFactory::createRedisDriver([
    'host' => '127.0.0.1',
    'port' => 6379,
    'database' => 0
]);

See Also: Cache Drivers Overview


13.5 Console API Quick Reference

This section provides a quick reference for Console Command methods.

BaseCommand Class

Base class for creating custom console commands.

Configuration Methods:

php
setName(string $name): self
setDescription(string $description): self
setHelp(string $help): self
addArgument(string $name, int $mode = InputArgument::OPTIONAL, string $description = '', mixed $default = null): self
addOption(string $name, ?string $shortcut = null, int $mode = InputOption::VALUE_NONE, string $description = '', mixed $default = null): self

Argument Modes:

  • InputArgument::REQUIRED: Argument is required
  • InputArgument::OPTIONAL: Argument is optional
  • InputArgument::IS_ARRAY: Argument accepts multiple values

Option Modes:

  • InputOption::VALUE_NONE: Option doesn't accept a value
  • InputOption::VALUE_REQUIRED: Option requires a value
  • InputOption::VALUE_OPTIONAL: Option value is optional
  • InputOption::VALUE_IS_ARRAY: Option accepts multiple values

Output Methods:

php
success(string $message): void
error(string $message): void
info(string $message): void
warning(string $message): void
note(string $message): void
comment(string $message): void
title(string $message): void
section(string $message): void
line(string $message): void
write(string $message): void
newLine(int $count = 1): void

Table Output:

php
table(array $headers, array $rows): void

Interactive Methods:

php
ask(string $question, ?string $default = null): mixed
confirm(string $question, bool $default = true): bool
choice(string $question, array $choices, mixed $default = null): mixed

Input Access:

php
argument(string $name): mixed
option(string $name): mixed

Exit Codes:

php
self::SUCCESS = 0
self::FAILURE = 1
self::INVALID = 2

Example:

php
use ElliePHP\Components\Console\BaseCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

final class MyCommand extends BaseCommand
{
    protected function configure(): void
    {
        $this->setName('my:command')
             ->setDescription('My custom command')
             ->addArgument('name', InputArgument::REQUIRED, 'The name')
             ->addOption('force', 'f', InputOption::VALUE_NONE, 'Force execution');
    }
    
    protected function handle(): int
    {
        $name = $this->argument('name');
        $force = $this->option('force');
        
        $this->info("Processing: {$name}");
        
        if ($this->confirm('Continue?')) {
            $this->success('Done!');
            return self::SUCCESS;
        }
        
        return self::FAILURE;
    }
}

See Also: Console Commands


Built-in Commands

serve - Start development server

bash
php ellie serve [--host=HOST] [--port=PORT] [--docroot=DOCROOT]

cache:clear - Clear cache

bash
php ellie cache:clear [--config] [--routes] [--views] [--all]

routes - List all registered routes

bash
php ellie routes

make:controller - Generate a controller class

bash
php ellie make:controller ControllerName [--resource] [--api]

See Also: Built-in Commands


Additional Resources


Copyright © 2024 ElliePHP Framework. Released under the MIT License.