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
- 3.1 Basic Routing
- 3.2 Route Parameters
- 3.3 Controller Routing
- 3.4 Closure-Based Routes
- 3.5 Route Middleware
- 3.6 Routes Command
4. Dependency Injection
- 4.1 Container Basics
- 4.2 Automatic Constructor Injection
- 4.3 Service Binding
- 4.4 Container Helper Function
- 4.5 Production Optimization
5. Middleware
- 5.1 PSR-15 Middleware Interface
- 5.2 Creating Custom Middleware
- 5.3 Middleware Registration
- 5.4 Middleware with Dependency Injection
- 5.5 Built-in Middleware
6. HTTP Request & Response
- 6.1 Request Class
- 6.2 Request Helper Function
- 6.3 Response Class Basics
- 6.4 Response Content Types
- 6.5 Response Redirects
- 6.6 Response Status Code Helpers
- 6.7 File Downloads
- 6.8 Headers and Cookies
- 6.9 Response Helper Function
- 6.10 Response Inspection
7. Caching
- 7.1 Cache Drivers Overview
- 7.2 File Cache Driver
- 7.3 Redis Cache Driver
- 7.4 SQLite Cache Driver
- 7.5 APCu Cache Driver
- 7.6 Basic Cache Operations
- 7.7 Batch Cache Operations
- 7.8 TTL and Expiration
- 7.9 Cache Helper Function
- 7.10 Cache Statistics
8. Console Commands
- 8.1 Built-in Serve Command
- 8.2 Built-in Cache:Clear Command
- 8.3 Built-in Routes Command
- 8.4 Built-in Make:Controller Command
- 8.5 Creating Custom Commands
- 8.6 Command Configuration
- 8.7 Command Output Methods
- 8.8 Interactive Commands
- 8.9 Command Registration
- 8.10 Command Exit Codes
9. Logging
- 9.1 Logging Basics
- 9.2 Log Levels
- 9.3 Context Parameters
- 9.4 Exception Logging
- 9.5 Report Helper Function
- 9.6 Log Configuration
10. Utilities & Helpers
- 10.1 String Utilities
- 10.2 File Utilities
- 10.3 JSON Utilities
- 10.4 Hash Utilities
- 10.5 Environment Utilities
- 10.6 Env Helper Function
- 10.7 Path Helper Functions
11. Configuration
- 11.1 Configuration File Structure
- 11.2 Environment Variables
- 11.3 Configuration Access
- 11.4 ConfigParser Class
- 11.5 Creating Custom Configuration Files
12. Advanced Topics
- 12.1 Service Layer Pattern
- 12.2 Repository Pattern
- 12.3 Framework Extension Points
- 12.4 Production Optimization
- 12.5 Error Handling Strategies
- 12.6 Security Best Practices
- 12.7 Testing Strategies
13. API Reference
- 13.1 Helper Functions Quick Reference
- 13.2 Utility Classes Quick Reference
- 13.3 HTTP API Quick Reference
- 13.4 Cache API Quick Reference
- 13.5 Console API Quick 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:
// 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()andhas()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
// 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
// Immutable controllers and services
final readonly class UserController
{
public function __construct(
private UserService $service,
private CacheInterface $cache
) {}
}Property Type Declarations
// Typed properties prevent runtime errors
private string $name;
private int $age;
private ?DateTime $createdAt = null;Union and Intersection Types
// Flexible type definitions
public function process(string|int $id): User|null
{
// ...
}Named Arguments
// Clear, self-documenting function calls
response()->json(
data: ['users' => $users],
status: 200,
headers: ['X-Total-Count' => count($users)]
);Attributes for Metadata
// 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
{
"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
{
"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
php -v
# Should show PHP 8.4.0 or higherCheck Required Extensions
php -m | grep -E "pdo|json|mbstring"
# Should list: PDO, json, mbstringCheck Optional Extensions
php -m | grep -E "redis|apcu|opcache"
# Lists installed optional extensionsVerify Composer
composer --version
# Should show Composer 2.0 or higherProduction Environment Recommendations
For production deployments, we recommend:
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
Web Server
- Enable HTTP/2 for better performance
- Configure proper SSL/TLS certificates
- Set up gzip/brotli compression
- Configure appropriate timeout values
Caching
- Use Redis or APCu for production caching
- Enable container compilation (APP_ENV=production)
- Configure route caching
- Enable OPcache with appropriate settings
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
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:
php -v
# Should show PHP 8.4.0 or higherVerify Composer is installed:
composer --version
# Should show Composer version 2.0 or higherInstallation Steps
1. Clone or Download the Framework
Clone the ElliePHP framework repository:
git clone https://github.com/elliephp/framework.git my-project
cd my-projectOr download and extract the framework archive to your project directory.
2. Install Dependencies
Install all required dependencies using Composer:
composer installThis 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:
cp .env.example .envOpen the .env file and configure your application settings:
# 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=5Key Configuration Options:
APP_NAME: Your application name (used in logs and responses)APP_DEBUG: Enable debug mode for development (set tofalsein production)APP_TIMEZONE: Default timezone for date/time operationsCACHE_DRIVER: Cache driver to use (file,redis,sqlite, orapcu)
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:
chmod -R 775 storageThe 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:
php ellie serveThen visit http://127.0.0.1:8000 in your browser. You should see a JSON response:
{
"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
pecl install redisThen enable the extension in your php.ini:
extension=redis.soAPCu Cache Driver
pecl install apcuThen enable the extension in your php.ini:
extension=apcu.so
apc.enabled=1
apc.shm_size=32MTroubleshooting Installation
Composer Install Fails
If composer install fails with memory errors:
php -d memory_limit=-1 /usr/local/bin/composer installPermission Denied Errors
If you encounter permission errors on the storage directory:
sudo chown -R $USER:$USER storage
chmod -R 775 storageMissing PHP Extensions
If you're missing required extensions, install them based on your system:
Ubuntu/Debian:
sudo apt-get install php8.4-pdo php8.4-json php8.4-mbstringmacOS (using Homebrew):
brew install php@8.4Port Already in Use
If port 8000 is already in use when starting the dev server:
php ellie serve --port=80802.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 documentationThe 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 servicesPurpose of Each Subdirectory:
Console/Command/: Custom CLI commands that extend theellieconsole applicationHttp/Controllers/: Controller classes that handle HTTP requests and return responsesHttp/Middleware/: Custom middleware for request/response processingServices/: Business logic and service layer classes
Namespace Convention:
All classes in the app/ directory use the ElliePHP\Framework\Application\ namespace:
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 registrationConfiguration Files:
app.php: Core application settings (name, debug mode, timezone)cache.php: Cache driver configuration and connection settingscontainer.php: Service bindings and dependency injection configurationmiddleware.php: Global middleware stack registration
Configuration files return PHP arrays and can access environment variables using the env() helper:
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.phpbootstraps 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 definitionsRoute File:
The router.php file defines all HTTP routes for your application:
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.phpFramework Components:
Kernel/: Application bootstrapping and request handlingSupport/: 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.logStorage 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:
chmod -R 775 storageThe tests/ Directory
PHPUnit test files for testing your application.
tests/
├── Feature/ # Feature/integration tests
└── Unit/ # Unit testsRun tests using:
composer testThe 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.gitignorefile - 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.phpServices
Organize services by domain:
app/Services/
├── User/
│ ├── UserService.php
│ └── UserRepository.php
└── Auth/
└── AuthenticationService.phpMiddleware
Keep middleware focused and single-purpose:
app/Http/Middleware/
├── AuthenticateMiddleware.php
├── RateLimitMiddleware.php
└── CorsMiddleware.phpAutoloading
ElliePHP uses PSR-4 autoloading configured in composer.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 theElliePHP\Framework\namespace - Classes in
app/use theElliePHP\Framework\Application\namespace - Helper functions are automatically loaded from
src/Support/
After adding new classes or namespaces, regenerate the autoloader:
composer dump-autoloadAdding New Directories
You can add custom directories to organize your code:
1. Create the directory:
mkdir -p app/Repositories2. Add classes with proper namespace:
<?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
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
readonlyclasses 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
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 routeRouter::post($path, $handler): Define a POST route{id}: Dynamic route parameter (automatically passed to controller method)[Controller::class, 'method']: Controller method handlerController::class: Single-method controller (calls__invokeor default method)
Step 3: Test Your Endpoints
Start the development server:
php ellie serveTest the GET /users endpoint:
curl http://127.0.0.1:8000/usersResponse:
{
"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:
curl http://127.0.0.1:8000/users/1Response:
{
"success": true,
"data": {
"id": 1,
"name": "Alice Johnson",
"email": "alice@example.com"
}
}Test with a non-existent user:
curl http://127.0.0.1:8000/users/999Response (404):
{
"success": false,
"error": "User not found"
}Test the POST /users endpoint:
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):
{
"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
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
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
UserServiceis 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:
- Request arrives at
public/index.php - Router matches the URL to a route definition
- Container resolves the controller and injects dependencies
- Controller method is called with route parameters
- Response is returned and sent to the client
HTTP Request → Router → Container → Controller → Response → HTTP ResponseNext 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:
php ellie serveThis 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) startedThe 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 ClosingCommand Options
The serve command supports several options to customize the server configuration:
Change the Host
php ellie serve --host=0.0.0.0This makes the server accessible from other devices on your network. Useful for testing on mobile devices or other computers.
Change the Port
php ellie serve --port=8080Or use the short option:
php ellie serve -p 8080Use this if port 8000 is already in use or you need to run multiple applications simultaneously.
Change the Document Root
php ellie serve --docroot=publicOr use the short option:
php ellie serve -d publicThe document root is the directory that contains your index.php file. The default is public/.
Combine Multiple Options
php ellie serve --host=0.0.0.0 --port=8080 --docroot=publicOr with short options:
php ellie serve -p 8080 -d publicCommand 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 messageCommon Use Cases
Local Development (Default)
php ellie serveAccess at: http://127.0.0.1:8000
Testing on Mobile Devices
php ellie serve --host=0.0.0.0Access from other devices using your computer's IP address:
- Find your IP:
ifconfig(macOS/Linux) oripconfig(Windows) - Access at:
http://192.168.1.100:8000(replace with your IP)
Running Multiple Applications
Terminal 1:
cd project1
php ellie serve --port=8000Terminal 2:
cd project2
php ellie serve --port=8001Access at:
- Project 1:
http://127.0.0.1:8000 - Project 2:
http://127.0.0.1:8001
Custom Port for Specific Services
php ellie serve --port=3000Useful 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
php ellie serve --port=8080Permission Denied
Error: Permission denied when starting server
Solution: Use a port above 1024 (ports below 1024 require root privileges)
php ellie serve --port=8000 # OK
php ellie serve --port=80 # Requires sudoCannot 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
php ellie serve --host=0.0.0.0Document Root Not Found
Error: Document root not found: public
Solution: Ensure you're in the project root directory or specify correct path
cd /path/to/project
php ellie serveAlternative: Using Other Web Servers
While the built-in server is convenient, you can also use other development servers:
PHP Built-in Server (Direct)
php -S 127.0.0.1:8000 -t publicUsing Docker
FROM php:8.4-cli
WORKDIR /app
COPY . /app
CMD ["php", "-S", "0.0.0.0:8000", "-t", "public"]Using Laravel Valet (macOS)
valet link
valet openUsing 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
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
/**
* 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
Router::get('/', WelcomeController::class);Static Pages
Router::get('/about', [PageController::class, 'about']);
Router::get('/contact', [PageController::class, 'contact']);
Router::get('/terms', [PageController::class, 'terms']);RESTful API Routes
// 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:
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}:
// 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:
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:
// 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:
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
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
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
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:
Router::get('/', WelcomeController::class);The controller must have a process() or __invoke() method:
<?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']:
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
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:
- Resolves the controller from the dependency injection container
- Injects dependencies into the controller constructor
- Extracts route parameters from the URL
- Calls the specified method with parameters
- Returns the response
Example Flow:
// 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 responseWelcomeController Example
The default ElliePHP installation includes a WelcomeController that demonstrates the single-method pattern:
<?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:
Router::get('/', WelcomeController::class);RESTful Controller Pattern
A common pattern is to create RESTful controllers with standard CRUD methods:
// 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']); // Delete3.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:
Router::get('/test', static function () {
return response()->json(['status' => 'ok']);
});Closure Routes with Response Helpers
Closures can use all response helper methods:
JSON Response
Router::get('/api/status', static function () {
return response()->json([
'status' => 'operational',
'timestamp' => time()
]);
});XML Response
Router::get('/sitemap.xml', static function () {
return response()->xml('<?xml version="1.0"?><urlset></urlset>');
});HTML Response
Router::get('/hello', static function () {
return response()->html('<h1>Hello, World!</h1>');
});Plain Text Response
Router::get('/robots.txt', static function () {
return response()->text("User-agent: *\nDisallow: /admin/");
});Redirect Response
Router::get('/old-page', static function () {
return response()->redirect('/new-page');
});Closures with Route Parameters
Closures can accept route parameters just like controller methods:
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
Router::get('/health', static function () {
return response()->json([
'status' => 'healthy',
'timestamp' => date('c')
]);
});API Version Info
Router::get('/api/version', static function () {
return response()->json([
'version' => '1.0.0',
'api_version' => 'v1'
]);
});Simple Redirect
Router::get('/docs', static function () {
return response()->redirect('https://github.com/elliephp/docs');
});Echo Parameter
Router::get('/echo/{message}', static function (string $message) {
return response()->text($message);
});Quick Test Route
Router::get('/test', static function () {
return response()->xml('<?xml version="1.0"?><root></root>');
});Closure Best Practices
- Keep closures simple - If logic exceeds 3-5 lines, use a controller
- Use static closures - Add
statickeyword for better performance - Type-hint parameters - Always specify parameter types
- Use response helpers - Leverage
response()helper for consistent responses - 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
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:
LoggingMiddlewareruns firstCorsMiddlewareruns second- Your route handler executes
- Response flows back through middleware in reverse order
Route-Specific Middleware
Apply middleware to specific routes using the third parameter:
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
Router::get('/profile', [ProfileController::class, 'show'], [
'middleware' => [AuthMiddleware::class]
]);
Router::put('/profile', [ProfileController::class, 'update'], [
'middleware' => [AuthMiddleware::class]
]);Multiple Middleware
Router::post('/api/posts', [PostController::class, 'store'], [
'middleware' => [
AuthMiddleware::class,
RateLimitMiddleware::class,
ValidateJsonMiddleware::class
]
]);Admin Routes with Middleware
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)
↓
ClientCreating Custom Middleware
Middleware must implement the PSR-15 MiddlewareInterface:
<?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
- Keep middleware focused - Each middleware should do one thing well
- Order matters - Place authentication before authorization
- Use global middleware sparingly - Only for truly universal concerns
- Consider performance - Middleware runs on every matching request
- 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:
php ellie routesCommand 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: 7Output 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
Router::get('/users', [UserController::class, 'index']);
// Output: UserController@indexSingle-Method Controller
Router::get('/', WelcomeController::class);
// Output: WelcomeController@processClosure Route
Router::get('/test', static function () {
return response()->json(['status' => 'ok']);
});
// Output: ClosureViewing Routes During Development
The routes command is particularly useful during development:
After Adding New Routes
php ellie routes
# Verify your new routes are registered correctlyDebugging Route Issues
php ellie routes
# Check if route exists and has correct HTTP methodAPI Documentation
php ellie routes > routes.txt
# Export route list for documentationRoute 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:
php ellie routesOutput:
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
NotFoundExceptionif service doesn't exist - Throws
ContainerExceptionif service cannot be resolved
has(string $id): bool
- Checks if a service is registered in the container
- Returns
trueif service exists,falseotherwise
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:
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.
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.
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.
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.
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
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
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
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; // trueResolving with Dependencies
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 injected4.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:
- Analyzes the constructor to identify required dependencies
- Resolves each dependency by looking up services in the container
- Recursively resolves dependencies of dependencies
- Creates the instance with all dependencies injected
- 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
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
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
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
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
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
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
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
readonlyclasses 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
/**
* 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
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:
final readonly class UserService
{
public function __construct(
private UserRepositoryInterface $repository // Gets UserRepository
) {}
}Multiple Interface Bindings
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
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
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
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
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
use function DI\create;
return [
CacheService::class => create(CacheService::class)->lazy(),
];Singleton with Constructor Parameters
use function DI\create;
return [
LogService::class => create(LogService::class)
->constructor(storage_logs_path('app.log'))
->lazy(),
];Multiple Singletons
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
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:
// 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:
use ElliePHP\Framework\Support\Container;
$container = Container::getInstance();Resolving Services
Pass a service identifier to container() to resolve it directly:
// Resolve a service
$userService = container(UserService::class);
// Use the service
$users = $userService->getAllUsers();This is a shorthand for:
$userService = container()->get(UserService::class);Practical Examples
In Controllers
While controllers use constructor injection, you might occasionally need to resolve services dynamically:
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:
/**
* 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:
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
// 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
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)
// ✅ Explicit, testable, clear dependencies
final readonly class UserService
{
public function __construct(
private UserRepository $repository,
private CacheInterface $cache
) {}
}Container Helper (When Needed)
// ✅ 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:
- Analysis Phase: PHP-DI analyzes all service definitions and dependencies
- Code Generation: Generates optimized PHP code for service resolution
- File Writing: Writes compiled container to
storage/Cache/CompiledContainer.php - 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
# Enable production optimizations
APP_ENV=production
# Other production settings
APP_DEBUG=false
CACHE_DRIVER=redisWhen 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:
// 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:
- Proxy Creation: PHP-DI generates a proxy class that extends your service
- Lazy Instantiation: The real service is only created when a method is called
- Transparent Usage: Proxies are transparent - your code doesn't know the difference
- Memory Savings: Services that aren't used are never instantiated
Example:
// 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 instantiatedProxy File Location:
Proxies are written to storage/Cache/proxies/ when APP_ENV=production:
storage/Cache/proxies/
├── ProxyExpensiveService.php
├── ProxyCacheService.php
└── ProxySessionManager.phpCache 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 filesCompiledContainer.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:
chmod -R 775 storage/CacheClearing Compiled Container
When you modify service bindings or container configuration, you need to clear the compiled container:
Using the Cache Clear Command:
# Clear all caches including container
php ellie cache:clear --all
# Clear only container cache
php ellie cache:clear --configManual Deletion:
# 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
APP_ENV=production
APP_DEBUG=false2. Clear Existing Cache
php ellie cache:clear --all3. Warm Up Cache (Optional)
# Make a request to generate compiled container
curl https://your-app.com/4. Verify Compilation
# Check that compiled container exists
ls -la storage/Cache/CompiledContainer.php
# Check proxy generation
ls -la storage/Cache/proxies/5. Set Proper Permissions
chmod -R 775 storage/Cache
chown -R www-data:www-data storage/CachePerformance 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=productionis set in.envstorage/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=productionis actually set - Check that
CompiledContainer.phpexists 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:
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:
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:
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:
// 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:
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:
return $response;Complete Middleware Flow Example
Here's a complete example showing the full request/response flow:
<?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 ResponseShort-Circuiting the Pipeline
Middleware can return a response without calling the handler, effectively short-circuiting the pipeline:
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:
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:
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:
# Example: Create an authentication middleware
touch app/Http/Middlewares/AuthMiddleware.phpStep 2: Define the Class Structure
Start with the basic class structure implementing MiddlewareInterface:
<?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
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:
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:
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
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
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
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)
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
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
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
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
/**
* 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:
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
::classconstant 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:
- Request Processing: Middleware processes requests from top to bottom
- Response Processing: Middleware processes responses from bottom to top
- Short-Circuiting: Earlier middleware can block later middleware from executing
Execution Flow Visualization:
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)
↓
ResponseOrder Matters: Practical Examples
Example 1: Logging Should Be First
Place logging middleware first to capture the entire request/response cycle:
return [
'global_middlewares' => [
LoggingMiddleware::class, // ✓ Captures everything
AuthMiddleware::class,
RateLimitMiddleware::class,
],
];If logging is last, it won't capture requests blocked by earlier middleware:
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:
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:
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:
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:
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
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
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:
php ellie servecurl -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
/**
* 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
$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
return [
'global_middlewares' => [
// Empty array - no middleware will run
],
];Or replace with your own:
<?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:
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:
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
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
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
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
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
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:
AuthServiceis 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
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
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:
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
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
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:
Access-Control-Allow-Origin: Specifies which origins can access the resource
- Default:
*(allows all origins) - For production, consider restricting to specific domains
- Default:
Access-Control-Allow-Methods: Specifies which HTTP methods are allowed
- Default:
GET, POST, PUT, DELETE, OPTIONS - Covers standard RESTful API operations
- Default:
Access-Control-Allow-Headers: Specifies which headers can be used
- Default:
Content-Type, Authorization - Allows JSON requests and authentication headers
- Default:
Configuration:
The default configuration allows all origins and common HTTP methods. To customize, modify the middleware:
Restrict to Specific Origins:
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:
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:
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:
CORS_ALLOWED_ORIGIN=https://yourdomain.com
CORS_ALLOWED_METHODS=GET, POST, PUT, DELETE, OPTIONS
CORS_ALLOWED_HEADERS=Content-Type, Authorization, X-Custom-HeaderAdditional CORS Headers:
For more advanced CORS configuration, you can add additional headers:
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
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:
{
"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:
curl -H "X-Correlation-ID: my-custom-id" http://localhost:8000/api/usersThe middleware will use the provided ID and include it in logs and the response.
Incoming Request without Correlation ID:
curl http://localhost:8000/api/usersThe 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: a1b2c3d4e5f6g7h8Benefits 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:
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
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:
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
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
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
return [
'global_middlewares' => [
// LoggingMiddleware removed
\ElliePHP\Framework\Application\Http\Middlewares\CorsMiddleware::class,
],
];To replace with custom version, use your own class:
<?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:
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:
$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:
// 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:
// 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:
// 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
// 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
// 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
// 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
// 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 customContent Type Detection
// 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
// 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:
// 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
// User agent
$userAgent = $request->userAgent();
// Referer
$referer = $request->referer();
$referrer = $request->referrer(); // AliasJSON Handling
Parse JSON request bodies:
// 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
// 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
// Get all cookies
$cookies = $request->cookies();
// Get specific cookie
$sessionId = $request->cookie('session_id');
// With default value
$theme = $request->cookie('theme', 'light');Server Parameters
// Get all server parameters
$server = $request->server();
// Get client IP
$ip = $request->ip();
// Get all client IPs (including proxies)
$ips = $request->ips();Security Methods
// Check if HTTPS
if ($request->isSecure()) {
// Secure connection
}Request Attributes
Store and retrieve custom attributes on the request:
// 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:
// Get PSR-7 ServerRequestInterface
$psrRequest = $request->psr();
$psrRequest = $request->raw(); // AliasComplete Example
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
// Get current request
$request = request();
// Access request data
$name = request()->input('name');
$email = request()->string('email');In Controllers
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
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
// 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
// Check bearer token
$token = request()->bearerToken();
if (!$token) {
return response()->unauthorized('Token required');
}
// Validate token
$user = validateToken($token);Content Negotiation
// Return appropriate response format
if (request()->wantsJson()) {
return response()->json($data);
} else {
return response()->html($htmlView);
}Query Parameters
// 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:
// 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
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:
// 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:
// 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:
// 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
// Set status code
$response = $response->withStatus(404, 'Not Found');
$response = $response->setStatusCode(404); // Alias
// Get status code
$code = $response->status();
$code = $response->getStatusCode(); // AliasSetting Body Content
// Set body content
$response = $response->withBody('New content');
$response = $response->setContent('New content'); // Alias
// Get body content
$content = $response->body();
$content = $response->content(); // AliasImmutability
PSR-7 responses are immutable - each modification returns a new instance:
$response1 = response()->make('Hello');
$response2 = $response1->withStatus(404);
// $response1 still has status 200
// $response2 has status 404
// They are different instancesMethod Chaining
Take advantage of immutability with method chaining:
return response()
->json(['message' => 'Success'])
->withHeader('X-Request-ID', $requestId)
->withStatus(201);Complete Example
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
// 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
// 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:
// 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:
/**/ typeof handleResponse === 'function' && handleResponse({"users":[...]});HTML Responses
Basic HTML
// 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
$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
// 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
// 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/jsonjsonp():text/javascripthtml():text/html; charset=utf-8text():text/plain; charset=utf-8xml():application/xml; charset=utf-8
You can override these with custom headers:
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)
// 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):
// 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:
// 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:
// 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):
// Redirect back
return response()->back();
// With fallback URL
return response()->back('/dashboard');
// With custom status
return response()->back(
fallback: '/home',
status: 302
);Use Cases
// 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
// 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
public function login(): ResponseInterface
{
$credentials = request()->only(['email', 'password']);
if (auth()->attempt($credentials)) {
return response()->redirect('/dashboard');
}
return response()->back('/login');
}After Resource Creation
public function store(): ResponseInterface
{
$user = User::create(request()->all());
// Redirect to view the created resource
return response()->redirectSeeOther('/users/' . $user->id);
}URL Migration
// 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
// 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:
// 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:
// Async processing accepted
return response()->accepted([
'message' => 'Job queued',
'job_id' => $jobId
]);204 No Content
Use when operation succeeds but no content to return:
// Successful deletion
return response()->noContent();
// Alias
return response()->empty();
// With custom status
return response()->noContent(204);4xx Client Error Responses
400 Bad Request
// Invalid request
return response()->badRequest('Invalid input');
// With validation errors
return response()->badRequest([
'message' => 'Validation failed',
'errors' => $errors
]);401 Unauthorized
// 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
// Access denied
return response()->forbidden('Access denied');
// With reason
return response()->forbidden([
'message' => 'Insufficient permissions',
'required_role' => 'admin'
]);404 Not Found
// 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
// Method not allowed
return response()->methodNotAllowed(
allowed: ['GET', 'POST'],
content: 'Method not allowed'
);
// Automatically sets Allow header
return response()->methodNotAllowed(['GET', 'HEAD']);409 Conflict
// Resource conflict
return response()->conflict('Email already exists');
// With details
return response()->conflict([
'message' => 'Resource conflict',
'field' => 'email',
'value' => $email
]);422 Unprocessable Entity
// 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
// 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
// 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
// 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
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
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
// 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-streamContent-Disposition: attachment; filename="document.txt"Content-Length: [size]
Download from File
File Download
// 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
$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:
// 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:
// Delete file after sending
return response()->streamDownload(
path: $tempFilePath,
filename: 'export.csv',
deleteAfter: true
);This is useful for temporary exports:
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:
// 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
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
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
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
// 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
// Add multiple headers
$response = response()->json($data)
->withHeaders([
'X-API-Version' => '1.0',
'X-Request-ID' => $requestId,
'X-RateLimit-Limit' => '100'
]);Content Type Helper
// 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
// 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
// Disable caching completely
$response = response()->json($data)
->noCache();This sets:
Cache-Control: no-cache, no-store, must-revalidatePragma: no-cacheExpires: 0
ETag and Last-Modified
ETag Header
// 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
// 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);
}Cookie Management
Setting Cookies
// 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
// 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
// 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
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
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
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
public function logout(): ResponseInterface
{
auth()->logout();
return response()->json([
'message' => 'Logged out successfully'
])->withoutCookie('auth_token');
}CORS Headers
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
// Get response instance with status code
$response = response(200);
// Default status is 200
$response = response();In Controllers
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
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:
// 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
// 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
// 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
// Redirect after form submission
return response()->redirect('/dashboard');
// Redirect back
return response()->back('/home');
// Permanent redirect
return response()->redirectPermanent('/new-url');HTML Responses
// 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:
// Each call creates new instance
$response1 = response();
$response2 = response();
// $response1 !== $response2 (different instances)This is different from request() which returns a singleton.
Complete Example
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
// Get status code
$code = $response->status();
$code = $response->getStatusCode(); // Alias
// Example
$response = response()->notFound('User not found');
echo $response->status(); // 404Status Code Checks
// 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
// 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
// Get body as string
$content = $response->body();
$content = $response->content(); // Alias
// Example
$response = response()->json(['message' => 'Hello']);
echo $response->body(); // {"message":"Hello"}JSON Encoding
// Get body as JSON string
$json = $response->toJson();
// Example
$response = response()->text('Hello World');
echo $response->toJson(); // "Hello World"Header Inspection
Getting Headers
// 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
$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
// 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
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
// 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
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
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:
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:
# 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=5The 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:
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:
// 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.jsonFile Content Example:
{
"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:
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:
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 hoursCaching Expensive Operations:
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:
// Automatically clears expired entries
$cache = cache('file');Manual Cleanup:
You can manually trigger garbage collection:
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:
// 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:
# Run cleanup daily at 2 AM
0 2 * * * cd /path/to/app && php ellie cache:cleanupPerformance 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:
- Use SSD Storage: Store cache files on SSD for faster I/O
- Limit Cache Size: Don't cache extremely large objects
- Set Appropriate TTLs: Shorter TTLs reduce disk usage
- Regular Cleanup: Schedule
clearExpired()to prevent disk bloat - Consider Alternatives: For high-traffic production, use Redis or APCu
File Permissions
Ensure the cache directory is writable by the web server:
# Set ownership
sudo chown -R www-data:www-data storage/Cache
# Set permissions
chmod -R 775 storage/CacheFor security, ensure the cache directory is not publicly accessible via the web server.
Troubleshooting
Permission Denied Errors:
// Ensure directory is writable
if (!is_writable(storage_cache_path())) {
throw new Exception('Cache directory is not writable');
}Disk Space Issues:
// 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:
# 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 -l7.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:
- Redis Server: Redis 5.0+ installed and running
- PHP Extension:
ext-redisPHP extension installed - Network Access: Application can connect to Redis server
Install Redis Extension:
# Using PECL
pecl install redis
# Enable in php.ini
extension=redis.soVerify Installation:
php -m | grep redis
# Should output: redisConfiguration
The Redis driver is configured using CacheFactory::createRedisDriver() with connection options:
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:
# Cache Configuration
CACHE_DRIVER=redis
# Redis Configuration
REDIS_HOST='127.0.0.1'
REDIS_PORT=6379
REDIS_PASSWORD=null
REDIS_DATABASE=0
REDIS_TIMEOUT=5When using the cache() helper with CACHE_DRIVER=redis, these environment variables are automatically used:
// 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:
$cache = cache('redis');
// Connection is established on first operation
$cache->set('key', 'value'); // Connects to Redis hereConnection Pooling:
The driver reuses connections within the same request, avoiding connection overhead:
$cache1 = cache('redis');
$cache2 = cache('redis');
// Both instances share the same Redis connection
$cache1->set('key1', 'value1');
$cache2->set('key2', 'value2');Connection Errors:
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:
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:
// 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:
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:
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:
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:
// 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:
// 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μsProduction Best Practices
1. Connection Pooling:
// 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:
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:
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:
// 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:
# Check if Redis is running
redis-cli ping
# Should return: PONG
# Check Redis status
sudo systemctl status redis
# Start Redis
sudo systemctl start redisAuthentication Errors:
# Set password in .env
REDIS_PASSWORD='your-secure-password'# Set password in Redis config
# Edit /etc/redis/redis.conf
requirepass your-secure-password
# Restart Redis
sudo systemctl restart redisMemory Issues:
# 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 redisNetwork Latency:
// 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:
php -m | grep -E "pdo|sqlite"
# Should output: PDO, pdo_sqliteThese extensions are typically enabled by default in PHP 8.4+.
Configuration
The SQLite driver is configured using CacheFactory::createSQLiteDriver():
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:
// 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:
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:
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:
// 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:
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:
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:
// Automatically clears expired entries
$cache = cache('sqlite');Manual Cleanup:
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:
// 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:
# Run cleanup daily at 3 AM
0 3 * * * cd /path/to/app && php ellie cache:cleanupPerformance 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μsBest Practices
1. Use SSD Storage:
// Store database on fast SSD
$driver = CacheFactory::createSQLiteDriver([
'path' => '/mnt/ssd/cache/cache.db'
]);2. Regular Maintenance:
// 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:
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:
// 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:
# 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:
// 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:
# 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.dbDatabase Corruption:
# 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 operationLarge Database Size:
// 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 persistence7.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:
# 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 apcuEnable in php.ini:
extension=apcu.so
apc.enabled=1
apc.shm_size=32M
apc.ttl=7200
apc.enable_cli=1Verify Installation:
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:
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:
# 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:
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:
# Use APCu in production
CACHE_DRIVER=apcu// Automatically uses APCu
$cache = cache(); // Uses CACHE_DRIVER from .envIn-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μs2. Zero Network Overhead:
Unlike Redis, APCu has no network latency:
// 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:
// 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 full4. Process-Shared Cache:
Cache is shared across all PHP processes (PHP-FPM workers, Apache processes):
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
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:
// 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:
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:
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:
// 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:
# 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=128M3. Monitor Memory Usage:
// 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:
# In deployment script
php -r "apcu_clear_cache();"
# Or via console command
php ellie cache:clear --allLimitations
Single-Server Only:
APCu cache is not shared across multiple servers:
// 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:
# Restart PHP-FPM
sudo systemctl restart php8.4-fpm
# All APCu cache is cleared
# Restart Apache
sudo systemctl restart apache2
# All APCu cache is clearedFor persistent cache, use SQLite or File driver.
Memory Limitations:
APCu is limited by configured shared memory size:
// 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:
# 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-fpmAPCu Not Enabled:
# Check configuration
php -i | grep apc.enabled
# Enable in php.ini
apc.enabled=1
# Restart PHP-FPM
sudo systemctl restart php8.4-fpmMemory Exhausted:
# Increase shared memory size in php.ini
apc.shm_size=128M
# Restart PHP-FPM
sudo systemctl restart php8.4-fpmCLI Not Working:
# Enable APCu for CLI in php.ini
apc.enable_cli=1Performance 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 | EasyAPCu 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:
public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): boolParameters:
- 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:
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:
// 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:
// 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:
public function get(string $key, mixed $default = null): mixedParameters:
- 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:
// 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 0Cache Miss Handling:
// 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:
// 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:
public function has(string $key): boolParameters:
- key (string): Cache key identifier
Returns: true if key exists and is not expired, false otherwise
Usage:
// 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:
// 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:
public function delete(string $key): boolParameters:
- key (string): Cache key identifier
Returns: true on success, false on failure
Usage:
// 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:
// 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:
public function clear(): boolReturns: true on success, false on failure
Usage:
// 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:
// 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:
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:
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:
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:
// 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:
// 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:
// 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 null4. Invalidate on Updates:
// 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:
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:
public function getMultiple(iterable $keys, mixed $default = null): iterableParameters:
- keys (iterable): Array or iterable of cache keys
- default (mixed): Default value for missing keys
Returns: Iterable of key-value pairs
Basic Usage:
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:
// 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:
// 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 operationReal-World Example:
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:
public function setMultiple(iterable $values, null|int|\DateInterval $ttl = null): boolParameters:
- 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:
$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 hourCaching Query Results:
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:
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:
// 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:
public function deleteMultiple(iterable $keys): boolParameters:
- keys (iterable): Array or iterable of cache keys to delete
Returns: true on success, false on failure
Basic Usage:
$cache = cache();
// Delete multiple users
$keys = ['user:123', 'user:456', 'user:789'];
$cache->deleteMultiple($keys);Invalidating Related Cache:
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:
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:
// 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 fasterComplete Example: Product Catalog
Here's a complete example using batch operations for a product catalog:
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:
// 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:
// 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 memory3. Handle Partial Failures:
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:
// 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:
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
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:
// 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:
// 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:
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:
// 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:
// 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:
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- YearsM- Months (before T) or Minutes (after T)D- DaysT- Time designator (separates date and time)H- HoursM- MinutesS- Seconds
Examples:
// 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:
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:
$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:
// 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:
// 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:
// 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); // Permanent2. Consider Cache Stampede:
// 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 gradually3. Use Shorter TTL for Critical Data:
// 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 hour4. Implement Soft Expiration:
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:
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:
// 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')); // nullDriver-Specific Expiration:
// 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 APCuTTL Examples by Use Case
Session Management:
// 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:
// 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:
// 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:
// 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:
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:
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
function cache(?string $cacheDriver = null): CacheParameters:
- cacheDriver (string|null): Cache driver name ('file', 'redis', 'sqlite', 'apcu'), or null to use
CACHE_DRIVERfrom environment
Returns: Cache instance configured with the specified driver
Basic Usage
Using Default Driver:
use function cache;
// Uses CACHE_DRIVER from .env
$cache = cache();
// Perform cache operations
$cache->set('key', 'value', 3600);
$value = $cache->get('key');Specifying Driver:
// 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:
# 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=5Driver Selection:
// .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 driverAutomatic Driver Configuration
The cache() function automatically configures each driver with appropriate settings:
File Driver Configuration:
// 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:
// 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:
// 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:
// 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):
// 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 expirationUsage Examples
Simple Caching:
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:
// 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:
// 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:
// 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.development
CACHE_DRIVER=file
# .env.production
CACHE_DRIVER=redis
REDIS_HOST='redis.production.com'
REDIS_PORT=6379
REDIS_PASSWORD='secure-password'// Same code works in both environments
$cache = cache();
$cache->set('data', $value, 3600);Testing Environment:
# .env.testing
CACHE_DRIVER=file// 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:
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:
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:
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:
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):
// Simple and clean
$cache = cache();
$cache->set('key', 'value', 3600);Direct Instantiation:
// 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:
// Good: Uses environment configuration
$cache = cache();
// Avoid: Hardcoded driver
$cache = cache('redis'); // Only when specifically needed2. Reuse Cache Instances:
// 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:
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:
/**
* 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:
// 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:
// 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:
# Check environment variable
php -r "echo getenv('CACHE_DRIVER');"
# Set temporarily
export CACHE_DRIVER=redis
# Or in .env file
echo "CACHE_DRIVER=redis" >> .envRedis Connection Failed:
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:
// 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:
// 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:
public function count(): intReturns: Total number of cache entries
Usage:
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:
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:
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:
public function size(): intReturns: Total cache size in bytes
Usage:
$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:
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:
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:
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:
$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 correctlyWhy Key Prefixing?
- Namespace Isolation: Prevents conflicts with other applications
- Easy Identification: Quickly identify ElliePHP cache entries
- Bulk Operations: Clear only ElliePHP cache entries
- Multi-Tenant: Run multiple ElliePHP apps on same cache server
Viewing Actual Keys:
# 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):
// 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:
$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:
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:
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:
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:
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:
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:
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 KBBest Practices
1. Monitor Cache Regularly:
// 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:
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:
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:
// 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:
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:
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
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:
php ellie serve
# Server starts at http://127.0.0.1:8000Specify custom host and port:
php ellie serve --host=0.0.0.0 --port=3000
# Server starts at http://0.0.0.0:3000Use short option syntax:
php ellie serve -p 9000
# Server starts at http://127.0.0.1:9000Custom document root:
php ellie serve --docroot=dist
# Serves files from the 'dist' directoryImplementation 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
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:
php ellie cache:clear
# or
php ellie cache:clear --allClear specific cache:
php ellie cache:clear --routesClear multiple specific caches:
php ellie cache:clear --config --routesOutput
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, viewsImplementation 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 cachestorage/Cache/routes/- Route cachestorage/Cache/views/- View cachestorage/Cache/- General cache directory
8.3 Built-in Routes Command
The routes command displays all registered routes in your application.
Command Syntax
php ellie routesThis command has no options or arguments.
Usage Example
php ellie routesOutput 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: 4Route 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
php ellie make:controller <name> [options]Arguments
name(required) - Controller name (e.g.,UserControlleror justUser)
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:
php ellie make:controller UserControllerGenerates a controller with a single process() method:
<?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:
php ellie make:controller PostController --resourceGenerates a controller with full CRUD methods:
process()- List all resourcescreate()- Show create formstore()- Store new resourceshow($id)- Show single resourceedit($id)- Show edit formupdate($id)- Update resourcedestroy($id)- Delete resource
Create an API resource controller:
php ellie make:controller ApiUserController --apiGenerates a controller with API-focused methods (no create/edit):
process()- List all resourcesshow($id)- Show single resourcestore()- Store new resourceupdate($id)- Update resourcedestroy($id)- Delete resource
Auto-append "Controller" suffix:
php ellie make:controller User
# Creates UserController.phpOutput
[OK] Controller created successfully!
[INFO] Location: app/Http/Controllers/UserController.phpImplementation 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
readonlyclass modifier for immutability
8.5 Creating Custom Commands
You can create custom console commands by extending the BaseCommand class.
Basic Command Structure
<?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 succeededself::FAILURE(1) - Command failedself::INVALID(2) - Invalid usage
Naming Conventions
Commands typically use colon-separated namespaces:
make:controller- Generator commandscache:clear- Cache-related commandsdb:migrate- Database commandsqueue: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
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.
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 providedInputArgument::OPTIONAL- Argument is optionalInputArgument::IS_ARRAY- Argument accepts multiple values
Accessing arguments:
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.
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 valueInputOption::VALUE_OPTIONAL- Option value is optionalInputOption::VALUE_IS_ARRAY- Option accepts multiple values
Option shortcuts: The second parameter is an optional shortcut (e.g., -a for --admin).
Accessing options:
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
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:
php ellie user:create john john@example.com --admin -p secret1238.7 Command Output Methods
The BaseCommand class provides rich output formatting methods.
Status Messages
// 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
// 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
// Title (large heading with underline)
$this->title('User Management');
// Section (smaller heading)
$this->section('Creating Users');Tables
Display data in formatted tables:
$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
// 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
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
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
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
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
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:
php ellie user:createThe 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
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
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:
php ellie list
# Shows all registered commands
php ellie user:create
# Runs your custom commandDependency Injection
Commands registered in Commands.php have access to the dependency injection container. You can inject dependencies through the constructor:
<?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:
// 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:
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:
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:
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:
#!/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
fiComplete Example
<?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:
# 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: 19. 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:
App Channel (
app.log) - For general application logging including:- Debug information
- Informational messages
- Warnings
- General errors
- HTTP request analytics
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
DEBUGand above - The exception logger is configured for
CRITICALlevel messages
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.
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.
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.
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.
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.
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:
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:
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:
// 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:
$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:
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
- Use consistent keys - Standardize context key names across your application
- Include identifiers - Always include relevant IDs (user_id, order_id, etc.)
- Add timestamps - Include timestamps for time-sensitive operations
- Avoid sensitive data - Never log passwords, tokens, or PII
- 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:
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
// 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:
// 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:
try {
$user = User::findOrFail($id);
} catch (ModelNotFoundException $e) {
report()->exception($e);
return response()->notFound();
}With additional context:
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:
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
// 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:
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:
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:
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:
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:
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:
// 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:
storage_logs_path('app.log') // storage/Logs/app.log
storage_logs_path('exceptions.log') // storage/Logs/exceptions.logYou can create additional log files for specific purposes:
storage_logs_path('security.log') // storage/Logs/security.log
storage_logs_path('performance.log') // storage/Logs/performance.logLog Level Filtering
Each handler can be configured with a minimum log level. Only messages at or above this level will be logged:
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::DEBUGLogLevel::INFOLogLevel::NOTICELogLevel::WARNINGLogLevel::ERRORLogLevel::CRITICALLogLevel::ALERTLogLevel::EMERGENCY
Custom Handlers
Monolog supports various handlers beyond file logging. You can add custom handlers to the logger:
Rotating file handler:
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:
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:
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:
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:
$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:
$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:
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:
// 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:
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:
Str::toPascalCase('hello-world'); // 'HelloWorld'
Str::toPascalCase('hello_world'); // 'HelloWorld'toSnakeCase()
Convert a string to snake_case format:
Str::toSnakeCase('HelloWorld'); // 'hello_world'
Str::toSnakeCase('helloWorld'); // 'hello_world'
Str::toSnakeCase('hello world'); // 'hello_world'toKebabCase()
Convert a string to kebab-case format:
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:
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:
Str::startsWith('Hello World', 'Hello'); // true
Str::startsWith('Hello World', 'World'); // falsestartsWithAny()
Check if a string starts with any of the given substrings:
Str::startsWithAny('Hello World', ['Hi', 'Hello']); // true
Str::startsWithAny('Hello World', ['Hi', 'Hey']); // falseendsWith()
Check if a string ends with a given substring:
Str::endsWith('Hello World', 'World'); // true
Str::endsWith('Hello World', 'Hello'); // falseendsWithAny()
Check if a string ends with any of the given substrings:
Str::endsWithAny('file.php', ['.php', '.js']); // truecontains()
Check if a string contains a given substring:
Str::contains('Hello World', 'World'); // true
Str::contains('Hello World', 'Goodbye'); // falsecontainsAny()
Check if a string contains any of the given substrings:
Str::containsAny('Hello World', ['World', 'Universe']); // truecontainsAll()
Check if a string contains all of the given substrings:
Str::containsAll('Hello World', ['Hello', 'World']); // true
Str::containsAll('Hello World', ['Hello', 'Goodbye']); // falselimit()
Limit the length of a string with an optional ending:
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:
Str::truncateWords('The quick brown fox jumps', 3); // 'The quick brown...'random()
Generate a random alphanumeric string:
Str::random(); // Random 16-character string
Str::random(32); // Random 32-character string
// Example: 'aB3xY9mK2pQ7wR5t'replace()
Replace all occurrences of a search string:
Str::replace('world', 'universe', 'Hello world'); // 'Hello universe'replaceFirst()
Replace the first occurrence of a search string:
Str::replaceFirst('a', 'X', 'banana'); // 'bXnana'replaceLast()
Replace the last occurrence of a search string:
Str::replaceLast('a', 'X', 'banana'); // 'bananX'before()
Get the portion of a string before a given value:
Str::before('user@example.com', '@'); // 'user'after()
Get the portion of a string after a given value:
Str::after('user@example.com', '@'); // 'example.com'beforeLast()
Get the portion before the last occurrence:
Str::beforeLast('path/to/file.txt', '/'); // 'path/to'afterLast()
Get the portion after the last occurrence:
Str::afterLast('path/to/file.txt', '/'); // 'file.txt'substr()
Extract a substring:
Str::substr('Hello World', 0, 5); // 'Hello'
Str::substr('Hello World', 6); // 'World'repeat()
Repeat a string multiple times:
Str::repeat('*', 5); // '*****'mask()
Mask a portion of a string:
Str::mask('4111111111111111', '*', 4, 8); // '4111********1111'
Str::mask('secret', '*'); // '******'extractStringBetween()
Extract text between two delimiters:
Str::extractStringBetween('Hello [World]', '[', ']'); // 'World'Validation Methods
isEmail()
Validate if a string is a valid email address:
Str::isEmail('user@example.com'); // true
Str::isEmail('invalid-email'); // falseisUrl()
Validate if a string is a valid URL:
Str::isUrl('https://example.com'); // true
Str::isUrl('not-a-url'); // falseisJson()
Validate if a string is valid JSON:
Str::isJson('{"name":"John"}'); // true
Str::isJson('invalid json'); // falseisAlphanumeric()
Check if string contains only alphanumeric characters:
Str::isAlphanumeric('abc123'); // true
Str::isAlphanumeric('abc-123'); // falseisAlpha()
Check if string contains only alphabetic characters:
Str::isAlpha('abcdef'); // true
Str::isAlpha('abc123'); // falseisNumeric()
Check if string is numeric:
Str::isNumeric('12345'); // true
Str::isNumeric('123.45'); // true
Str::isNumeric('abc'); // falseisEmpty()
Check if a string is empty (after trimming):
Str::isEmpty(' '); // true
Str::isEmpty('Hello'); // falseisNotEmpty()
Check if a string is not empty:
Str::isNotEmpty('Hello'); // true
Str::isNotEmpty(' '); // falseAdditional String Methods
length()
Get the length of a string (multibyte safe):
Str::length('Hello'); // 5
Str::length('こんにちは'); // 5wordCount()
Count the number of words in a string:
Str::wordCount('Hello World'); // 2words()
Get the first N words from a string:
Str::words('The quick brown fox', 2); // 'The quick'clean()
Remove special characters, keeping only letters, numbers, and spaces:
Str::clean('Hello! @World#'); // 'Hello World'trim(), ltrim(), rtrim()
Remove whitespace from strings:
Str::trim(' Hello '); // 'Hello'
Str::ltrim(' Hello'); // 'Hello'
Str::rtrim('Hello '); // 'Hello'removePrefix()
Remove a prefix from a string:
Str::removePrefix('HelloWorld', 'Hello'); // 'World'removeSuffix()
Remove a suffix from a string:
Str::removeSuffix('HelloWorld', 'World'); // 'Hello'ensurePrefix()
Ensure a string starts with a prefix:
Str::ensurePrefix('World', 'Hello'); // 'HelloWorld'
Str::ensurePrefix('HelloWorld', 'Hello'); // 'HelloWorld'ensureSuffix()
Ensure a string ends with a suffix:
Str::ensureSuffix('Hello', 'World'); // 'HelloWorld'
Str::ensureSuffix('HelloWorld', 'World'); // 'HelloWorld'toArray()
Convert a string to an array of characters:
Str::toArray('Hello'); // ['H', 'e', 'l', 'l', 'o']split()
Split a string by a delimiter:
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:
Str::swap('Hello {name}!', ['{name}' => 'John']); // 'Hello John!'match()
Execute a regular expression match:
$matches = Str::match('/\d+/', 'Order 123');
// ['123']matchAll()
Execute a global regular expression match:
$matches = Str::matchAll('/\d+/', 'Order 123 and 456');
// [['123', '456']]plural()
Convert a word to plural form (basic English rules):
Str::plural('user'); // 'users'
Str::plural('child'); // 'children'
Str::plural('person'); // 'people'singular()
Convert a word to singular form:
Str::singular('users'); // 'user'
Str::singular('children'); // 'child'Practical Examples
Building a URL Slug
$title = 'My First Blog Post!';
$slug = Str::slug($title);
// 'my-first-blog-post'Masking Sensitive Data
$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
$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
$apiKey = Str::random(32);
$verificationCode = Str::random(6);Text Processing
$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); // 910.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:
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 readput()
Write contents to a file (creates or overwrites):
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:
File::append('/path/to/log.txt', "New log entry\n");
// Returns number of bytes written
// Creates file if it doesn't existprepend()
Prepend contents to the beginning of a file:
File::prepend('/path/to/file.txt', "Header\n");
// Returns number of bytes writtenFile Information Methods
exists()
Check if a file or directory exists:
if (File::exists('/path/to/file.txt')) {
// File exists
}isFile()
Check if a path is a file:
if (File::isFile('/path/to/file.txt')) {
// It's a file, not a directory
}isDirectory()
Check if a path is a directory:
if (File::isDirectory('/path/to/directory')) {
// It's a directory
}size()
Get the file size in bytes:
$bytes = File::size('/path/to/file.txt');
// Returns integer (file size in bytes)humanSize()
Get human-readable file size:
$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:
$mime = File::mimeType('/path/to/image.jpg');
// 'image/jpeg'
$mime = File::mimeType('/path/to/document.pdf');
// 'application/pdf'extension()
Get the file extension:
$ext = File::extension('/path/to/file.txt');
// 'txt'name()
Get the file name without extension:
$name = File::name('/path/to/file.txt');
// 'file'basename()
Get the file name with extension:
$basename = File::basename('/path/to/file.txt');
// 'file.txt'dirname()
Get the directory name of a path:
$dir = File::dirname('/path/to/file.txt');
// '/path/to'lastModified()
Get the last modification time as Unix timestamp:
$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:
if (File::isOlderThan('/path/to/cache.txt', 3600)) {
// File is older than 1 hour
}File Permission Methods
isReadable()
Check if a file is readable:
if (File::isReadable('/path/to/file.txt')) {
$content = File::get('/path/to/file.txt');
}isWritable()
Check if a file is writable:
if (File::isWritable('/path/to/file.txt')) {
File::put('/path/to/file.txt', 'New content');
}permissions()
Get file permissions:
$perms = File::permissions('/path/to/file.txt');
// Returns integer (e.g., 33188 for 0644)chmod()
Set file permissions:
File::chmod('/path/to/file.txt', 0644);
File::chmod('/path/to/script.sh', 0755);JSON File Methods
json()
Read and decode a JSON file:
$data = File::json('/path/to/config.json');
// Returns associative array
$data = File::json('/path/to/config.json', false);
// Returns objectputJson()
Encode and write data to a JSON file:
$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:
File::copy('/path/to/source.txt', '/path/to/destination.txt');
// Returns true on successmove()
Move a file to a new location:
File::move('/path/to/old.txt', '/path/to/new.txt');
// Returns true on successdelete()
Delete a file:
File::delete('/path/to/file.txt');
// Returns true on success
// Returns true if file doesn't exist (idempotent)replace()
Replace content in a file:
File::replace('/path/to/file.txt', 'old text', 'new text');
// Returns number of bytes writtenreplaceRegex()
Replace content using regular expressions:
File::replaceRegex('/path/to/file.txt', '/\d+/', 'NUMBER');
// Replaces all numbers with 'NUMBER'contains()
Check if a file contains a string:
if (File::contains('/path/to/file.txt', 'search term')) {
// File contains the search term
}hash()
Generate a hash of file contents:
$hash = File::hash('/path/to/file.txt');
// SHA256 hash by default
$hash = File::hash('/path/to/file.txt', 'md5');
// MD5 hashDirectory Methods
makeDirectory()
Create a directory:
File::makeDirectory('/path/to/new/directory');
// Creates nested directories by default with 0755 permissions
File::makeDirectory('/path/to/directory', 0777, false);
// Non-recursive, custom permissionsfiles()
Get all files in a directory:
$files = File::files('/path/to/directory');
// Returns array of file paths
$files = File::files('/path/to/directory', true);
// Recursive searchdirectories()
Get all subdirectories in a directory:
$dirs = File::directories('/path/to/directory');
// Returns array of directory pathsdeleteDirectory()
Delete a directory and its contents:
File::deleteDirectory('/path/to/directory');
// Deletes directory and all contents
File::deleteDirectory('/path/to/directory', true);
// Preserves the directory, only deletes contentscleanDirectory()
Remove all contents from a directory:
File::cleanDirectory('/path/to/directory');
// Removes all files and subdirectories, keeps the directory itselfcopyDirectory()
Copy an entire directory:
File::copyDirectory('/path/to/source', '/path/to/destination');
// Recursively copies all files and subdirectoriesmoveDirectory()
Move an entire directory:
File::moveDirectory('/path/to/source', '/path/to/destination');
// Copies then deletes sourceAdvanced Methods
lines()
Get file contents as an array of lines:
$lines = File::lines('/path/to/file.txt');
// Returns array of lines
$lines = File::lines('/path/to/file.txt', true);
// Skip empty linesglob()
Find files matching a pattern:
$files = File::glob('/path/to/*.txt');
// Returns array of matching file paths
$files = File::glob('/path/to/**/*.php');
// Recursive pattern matchingmatchesPattern()
Check if a path matches a pattern:
if (File::matchesPattern('*.txt', 'file.txt')) {
// Matches
}relativePath()
Get relative path from one file to another:
$relative = File::relativePath('/var/www/app', '/var/www/public');
// '../public'ensureExists()
Ensure a file exists, create if it doesn't:
$created = File::ensureExists('/path/to/file.txt', 'Initial content');
// Returns true if created, false if already existedclosestExistingDirectory()
Find the closest existing parent directory:
$dir = File::closestExistingDirectory('/path/that/may/not/exist');
// Returns '/path/that' if '/path/that/may/not/exist' doesn't existPractical Examples
Reading Configuration Files
// 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 linesWriting Log Files
$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
$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
$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
// 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
$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
$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:
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:
$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):
$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):
$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:
if (Json::isValid('{"name":"John"}')) {
// Valid JSON
}
if (!Json::isValid('invalid json')) {
// Invalid JSON
}lastError()
Get the last JSON error message:
$data = Json::safeDecode('invalid json');
if ($data === null) {
$error = Json::lastError();
// 'Syntax error'
}lastErrorCode()
Get the last JSON error code:
$code = Json::lastErrorCode();
// JSON_ERROR_NONE, JSON_ERROR_SYNTAX, etc.Pretty Printing
pretty()
Format JSON with pretty printing:
$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:
$compact = '{"name":"John","age":30}';
$pretty = Json::format($compact);
// Pretty-printed versionminify()
Remove unnecessary whitespace from JSON:
$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:
$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:
$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 structurehas()
Check if a key exists using dot notation:
$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:
$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:
$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 invalidtoFile()
Encode and write JSON to a file:
$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 errorUtility Methods
merge()
Merge multiple JSON strings or arrays:
$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:
$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:
$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:
$json = '{"name":"John","age":30,"city":"NYC"}';
$filtered = Json::only($json, ['name', 'age']);
// '{"name":"John","age":30}'except()
Remove specific keys from JSON:
$json = '{"name":"John","age":30,"city":"NYC"}';
$filtered = Json::except($json, ['age']);
// '{"name":"John","city":"NYC"}'validate()
Validate JSON against a basic schema:
$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:
$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:
$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 headersPractical Examples
API Response Handling
// 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
// 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
// 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
// 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
// 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
// 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
// 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:
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 slowercheck()
Verify a password against a hash:
$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):
$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:
$info = Hash::info($hash);
/*
[
'algo' => 1, // PASSWORD_BCRYPT
'algoName' => 'bcrypt',
'options' => ['cost' => 12]
]
*/argon2i()
Hash using Argon2i algorithm:
$hash = Hash::argon2i($password);
// Argon2i hash
$hash = Hash::argon2i($password, [
'memory_cost' => 2048,
'time_cost' => 4,
'threads' => 3
]);argon2id()
Hash using Argon2id algorithm (recommended):
$hash = Hash::argon2id($password);
// Argon2id hash (most secure)Hash Algorithms
sha256()
Generate a SHA256 hash:
$hash = Hash::sha256('Hello World');
// 'a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e'sha512()
Generate a SHA512 hash:
$hash = Hash::sha512('Hello World');
// Longer hash stringmd5()
Generate an MD5 hash (not recommended for security):
$hash = Hash::md5('Hello World');
// 'b10a8db164e0754105b7a99be72e3fe5'sha1()
Generate a SHA1 hash:
$hash = Hash::sha1('Hello World');
// '0a4d55a8d778e5022fab701977c5d840bbc486d0'xxh3()
Generate an XXH3 hash (fast, non-cryptographic):
$hash = Hash::xxh3('Hello World');
// Fast hash for checksums and hash tablescrc32()
Generate a CRC32 hash:
$hash = Hash::crc32('Hello World');
// Fast checksumhash()
Generate a hash using any algorithm:
$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:
$algos = Hash::algorithms();
// ['md5', 'sha1', 'sha256', 'sha512', 'xxh3', ...]HMAC Hashing
hmac()
Generate an HMAC hash with a secret key:
$message = 'Important data';
$secretKey = 'my-secret-key';
$hmac = Hash::hmac($message, $secretKey);
// HMAC-SHA256 by default
$hmac = Hash::hmac($message, $secretKey, 'sha512');
// HMAC-SHA512File Hashing
file()
Generate a hash of a file:
$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:
$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):
$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:
$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:
$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):
$known = 'secret-token-123';
$user = request()->header('X-API-Token');
if (Hash::equals($known, $user)) {
// Tokens match
}checksum()
Generate a checksum for data integrity:
$data = 'Important data';
$checksum = Hash::checksum($data);
// SHA256 checksum
$checksum = Hash::checksum($data, 'md5');
// MD5 checksumverifyChecksum()
Verify a checksum:
$data = 'Important data';
$checksum = Hash::checksum($data);
if (Hash::verifyChecksum($data, $checksum)) {
// Data integrity verified
}Encoding Methods
base64()
Generate a base64-encoded hash:
$hash = Hash::base64('Hello World');
// Base64-encoded SHA256 hashbase64Url()
Generate a URL-safe base64-encoded hash:
$hash = Hash::base64Url('Hello World');
// URL-safe base64 (no +, /, or = characters)short()
Generate a short hash (for URLs, etc.):
$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:
$salt = Hash::random(16);
$hash = Hash::salted('password', $salt);
// Hash with salt prependedPractical Examples
User Authentication
// 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
// 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
// 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
// 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
// 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
// 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
// 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:
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:
$env = new Env(root_path());
$env->load();
// Variables are now available via $env->get()loadWithRequired()
Load environment variables and require specific variables:
$env = new Env(root_path());
$env->loadWithRequired(['APP_NAME', 'APP_ENV', 'APP_DEBUG']);
// Throws exception if any required variable is missingisLoaded()
Check if environment variables have been loaded:
if (!$env->isLoaded()) {
$env->load();
}Get Method with Automatic Type Casting
get()
Get an environment variable with automatic type casting:
// 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 setType Casting Based on Default Value
The get() method automatically casts the return value to match the type of the default value:
// 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 nullSmart 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:
// 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:
// 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:
// 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:
// 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
// In .env file:
// OPTIONAL_VALUE=null
// ANOTHER_VALUE=(null)
$value = $env->get('OPTIONAL_VALUE'); // null
$another = $env->get('ANOTHER_VALUE'); // nullEmpty String Values
// 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
// 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:
// 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:
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:
$env->load();
$env->require(['APP_NAME', 'APP_ENV']);
// Throws exception if any variable is missingrequireNotEmpty()
Require variables to be set and not empty:
$env->load();
$env->requireNotEmpty(['DATABASE_HOST', 'DATABASE_NAME']);
// Throws exception if any variable is missing or emptyrequireOneOf()
Require a variable to be one of specific values:
$env->load();
$env->requireOneOf('APP_ENV', ['local', 'staging', 'production']);
// Throws exception if APP_ENV is not one of the allowed valuesAll Method
all()
Get all environment variables:
$allVars = $env->all();
// Returns associative array of all environment variablesPractical Examples
Basic Environment Setup
// 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
$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
$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
$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
$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
$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
// 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
$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
$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
// 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 setUsage With and Without Parameters
Without Parameters
When called without parameters, env() returns the Env instance:
// 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:
// 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:
- Fallback Value: Returned if the environment variable doesn't exist
- Type Hint: Determines the return type through automatic casting
String Default
$name = env('APP_NAME', 'ElliePHP');
// Returns string, defaults to 'ElliePHP'
$host = env('DATABASE_HOST', 'localhost');
// Returns string, defaults to 'localhost'Boolean Default
$debug = env('APP_DEBUG', false);
// Returns boolean, defaults to false
$maintenance = env('MAINTENANCE_MODE', true);
// Returns boolean, defaults to trueInteger Default
$port = env('APP_PORT', 8000);
// Returns integer, defaults to 8000
$maxConnections = env('MAX_CONNECTIONS', 100);
// Returns integer, defaults to 100Float Default
$timeout = env('TIMEOUT', 30.0);
// Returns float, defaults to 30.0
$taxRate = env('TAX_RATE', 0.15);
// Returns float, defaults to 0.15Null Default
$optional = env('OPTIONAL_VALUE', null);
// Returns value as-is or null (no type casting)Practical Examples
Application Configuration
// 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
$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
$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
$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
// 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
// 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
$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
$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
$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
$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
// 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
// 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); // mixedUsing Env Instance Methods
// 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
// 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:
// 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:
// 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):
// 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:
// 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):
// 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:
// 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):
// 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:
// 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):
// 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:
// 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):
// 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:
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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 configurationEach 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
/**
* 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
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
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
return [
'required_configs' => [
'APP_NAME',
'APP_TIMEZONE',
'APP_ENV',
],
];Middleware.php - Global middleware stack:
<?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:
# 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=5Available Environment Variables
Here are all the environment variables available in ElliePHP, based on .env.example:
Application Settings
| Variable | Type | Default | Description |
|---|---|---|---|
APP_NAME | string | 'ElliePHP' | Application name used in logs and error messages |
APP_DEBUG | boolean | true | Enable debug mode for detailed error messages |
APP_TIMEZONE | string | 'UTC' | Default timezone for date/time operations |
APP_ENV | string | - | Application environment (development, production) |
Cache Configuration
| Variable | Type | Default | Description |
|---|---|---|---|
CACHE_DRIVER | string | 'file' | Cache driver to use (file, redis, sqlite, apcu) |
Redis Configuration
These variables are used when CACHE_DRIVER=redis:
| Variable | Type | Default | Description |
|---|---|---|---|
REDIS_HOST | string | '127.0.0.1' | Redis server hostname or IP address |
REDIS_PORT | integer | 6379 | Redis server port |
REDIS_PASSWORD | string | null | Redis authentication password (null for no auth) |
REDIS_DATABASE | integer | 0 | Redis database number (0-15) |
REDIS_TIMEOUT | integer | 5 | Connection 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
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:
// 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 valuesnull- Converted to PHP nullempty- Converted to empty string- Quoted strings - Quotes are removed:
'value'becomesvalue - Numbers - Automatically cast to integers or floats
# 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.5Environment Variable Type Casting
The env() function automatically casts values based on the default value type:
// 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:
- Add the variable to your
.envfile:
# Custom Configuration
MY_API_KEY='your-api-key-here'
MY_API_URL='https://api.example.com'- If the variable is required, add it to
configs/Env.php:
return [
'required_configs' => [
'APP_NAME',
'APP_TIMEZONE',
'APP_ENV',
'MY_API_KEY', // Add your required variable
],
];- Access the variable using the
env()helper:
$apiKey = env('MY_API_KEY');
$apiUrl = env('MY_API_URL', 'https://default-api.com');Best Practices
- Never commit
.envfiles: Add.envto.gitignoreto prevent sensitive data from being committed - Use
.env.example: Maintain a.env.examplefile with all variables (using placeholder values) as documentation - Document required variables: Always update
configs/Env.phpwhen 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:
// 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:
// 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
nullif any segment doesn't exist (unless a default is provided)
Setting Configuration Values
You can set configuration values at runtime using the config() function:
// 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:
$configParser = config();
// Use ConfigParser methods directly
$allConfig = $configParser->all();
$hasKey = $configParser->has('Middleware.global_middlewares');Practical Examples
Example 1: Accessing Command Configuration
// 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
// 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
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
// Temporarily override configuration for testing
config(['app.debug' => true]);
// Perform operations with debug enabled
$result = someOperation();
// Configuration change only affects current requestExample 5: Checking Configuration Existence
// 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:
// 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
ConfigParseris 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
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
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 instanceload() Method
Load a single configuration file by name (without the .php extension):
// 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:
public function load(string $name): voidParameters:
$name- Configuration file name without.phpextension
Throws:
RuntimeException- If the configuration file doesn't exist
loadAll() Method
Load all configuration files from the configuration directory:
// 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:
public function loadAll(): voidThis 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:
// 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'); // nullMethod Signature:
public function get(string $key, mixed $default = null): mixedParameters:
$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
$defaultif the key doesn't exist
Dot Notation Parsing:
The get() method parses dot notation keys to navigate nested arrays:
// 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:
// 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:
public function set(string $key, mixed $value): voidParameters:
$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:
// 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'); // falseMethod Signature:
public function has(string $key): boolParameters:
$key- Dot notation key to check
Returns:
trueif the key exists,falseotherwise
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:
// 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:
public function all(): arrayReturns:
- 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
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
// 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
// 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 requestExample 4: Configuration Validation
// 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
// 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:
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
Use the config() Helper: Instead of creating
ConfigParserinstances directly, use theconfig()helper which manages a singleton instanceLoad Configuration Early: Load all configuration during application bootstrap using
loadAll()Check Existence Before Access: Use
has()to check if optional configuration exists before accessing itProvide Defaults: Always provide sensible defaults when using
get()for optional configurationDon't Persist Runtime Changes: Remember that
set()only affects the current request; don't rely on it for persistent configuration changesUse Dot Notation Consistently: Stick to dot notation for all configuration access to maintain consistency
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.phpnotDatabases.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.phpAvoid:
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
// 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:
// 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:
// 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:
// Even if Database.php wasn't loaded yet, this works
$host = config('Database.connections.mysql.host');
// The framework automatically loads Database.php when neededConfiguration File Structure Best Practices
Use Nested Arrays for Organization
<?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
// 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
// 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
// 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:
// 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
// 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
// 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:
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:
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
Group Related Settings: Keep related configuration in the same file
Use Environment Variables: Reference environment variables for values that change between environments
Provide Defaults: Always provide sensible defaults using the second parameter of
env()Document Options: Add comments explaining what each configuration option does
Keep It Simple: Don't over-complicate configuration structure; flat is better than nested when possible
Validate Early: Validate required configuration during application bootstrap
Use Type Hints: When accessing configuration in classes, type hint the expected values
Version Control: Commit configuration files to version control (but not
.envfiles)Test Configuration: Write tests to ensure configuration is loaded correctly
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 AccessControllers 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:
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:
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:
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:
// Good: Focused services
UserService
OrderService
PaymentService
EmailService
// Bad: Monolithic service
ApplicationService // handles everything2. Use Dependency Injection
Always inject dependencies through the constructor, never use global state or service locators:
// 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:
// 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:
// Good: Accept data
public function createUser(array $data): array
// Bad: Accept HTTP request
public function createUser(ServerRequestInterface $request): array5. Throw Exceptions for Business Rule Violations
Use exceptions to signal business rule violations, let controllers handle HTTP responses:
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:
declare(strict_types=1);
public function getUserById(int $id): ?array
{
// Implementation
}7. Make Services Readonly
Use readonly classes to ensure immutability:
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:
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/CacheServices 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:
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:
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
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:
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:
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:
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:
// 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:
// 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:
// 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:
// Good: Return array
public function findById(int $id): ?array
// Bad: Return PDOStatement
public function findById(int $id): PDOStatement3. Keep Repositories Focused
One repository per entity or aggregate root:
// Good: Focused repositories
UserRepository
OrderRepository
ProductRepository
// Bad: Generic repository
GenericRepository // handles all entities4. Don't Put Business Logic in Repositories
Repositories should only handle data access, not business rules:
// 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:
declare(strict_types=1);
public function findById(int $id): ?array
{
// Implementation
}6. Make Repositories Readonly
Use readonly classes when possible:
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:
// 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:
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:
- Custom Middleware - Add request/response processing logic
- Custom Console Commands - Create CLI tools for your application
- Custom Service Providers - Register and configure services
- Custom Utilities - Add helper functions and utility classes
- Custom Cache Drivers - Implement alternative caching backends
- 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
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
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
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
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
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
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
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
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
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
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:
{
"autoload": {
"files": [
"src/Support/path.php",
"src/Support/helpers.php",
"app/Support/helpers.php"
]
}
}Then run:
composer dump-autoloadExample: Custom Utility Class
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
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
// 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
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
// 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:
public function __construct(
private readonly UserRepository $repository,
private readonly CacheInterface $cache
) {}3. Make Extensions Configurable
Use environment variables and configuration files:
$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:
/**
* 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 MiddlewareInterface5. Write Tests
Test your extensions to ensure they work correctly:
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:
- Container Compilation - Pre-compile dependency injection container
- Route Caching - Cache route definitions for faster matching
- Cache Driver Selection - Choose optimal cache backend
- PHP Configuration - Optimize PHP settings for production
- Autoloader Optimization - Generate optimized class maps
- 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:
APP_ENV=production
APP_DEBUG=falseWhen 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 codeClear Compiled Container
After deploying code changes, clear the compiled container:
php ellie cache:clear --configOr manually delete the file:
rm storage/Cache/CompiledContainer.phpContainer 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
// 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].cacheClear Route Cache
After modifying routes, clear the cache:
php ellie cache:clear --routesRoute 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
| Driver | Speed | Persistence | Multi-Server | Best For |
|---|---|---|---|---|
| APCu | Fastest | No (memory) | No | Single-server, high-performance |
| Redis | Very Fast | Yes | Yes | Multi-server, distributed systems |
| File | Fast | Yes | No | Simple deployments, shared hosting |
| SQLite | Moderate | Yes | No | Embedded applications |
Recommended Production Drivers
For Single-Server Deployments:
Use APCu for maximum performance:
CACHE_DRIVER=apcuInstall APCu extension:
pecl install apcuEnable in php.ini:
extension=apcu.so
apc.enabled=1
apc.shm_size=64M
apc.ttl=7200
apc.gc_ttl=3600For Multi-Server Deployments:
Use Redis for shared caching across servers:
CACHE_DRIVER=redis
REDIS_HOST=your-redis-server.com
REDIS_PORT=6379
REDIS_PASSWORD=your-secure-password
REDIS_DATABASE=0For Shared Hosting:
Use file cache (default):
CACHE_DRIVER=fileEnsure storage/Cache/ is writable:
chmod -R 775 storage/CacheCache Configuration Example
// 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
; 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 = 20OPcache Configuration
OPcache is critical for production performance. It caches compiled PHP bytecode in memory.
Enable OPcache:
# Check if OPcache is enabled
php -i | grep opcache.enable
# If not enabled, add to php.ini
zend_extension=opcache.so
opcache.enable=1OPcache Benefits:
- 5-10x faster PHP execution
- Reduced CPU usage (no recompilation)
- Lower disk I/O (bytecode cached in memory)
Clear OPcache After Deployment:
# 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:
composer install --no-dev --optimize-autoloader --classmap-authoritativeFlags 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:
composer dump-autoload --optimize --classmap-authoritative6. Error Handling and Logging
Configure proper error handling for production.
Disable Debug Mode
APP_ENV=production
APP_DEBUG=falseWhen 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:
// Logs are automatically written to:
storage/Logs/app.log # Application logs
storage/Logs/exceptions.log # Exception logsLog Rotation
Set up log rotation to prevent disk space issues:
# /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:
// 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.logSet Proper Permissions
# 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
# 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 cache8. 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_limitappropriately - [ ] 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/andstorage/Logs/ - [ ] Verify
.envfile is not publicly accessible
Security
- [ ] Ensure
.envis 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
// 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:
$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
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:
$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:
composer update --no-dev9. 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:
- Global Exception Handler - Catches all uncaught exceptions
- Bootstrap Exception Handling - Handles errors during application startup
- Runtime Exception Handling - Handles errors during request processing
- Debug vs Production Modes - Different error responses based on environment
- 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
// 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 ResponseBootstrap 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:
// 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:
{
"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:
// 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:
{
"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:
APP_ENV=debug
APP_DEBUG=trueDebug 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:
{
"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:
APP_ENV=production
APP_DEBUG=falseProduction 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:
{
"code": 500,
"status": "error",
"message": "An unexpected error occurred"
}Checking Debug Mode in Code:
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:
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
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:
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:
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
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:
// 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:
{
"error": "Human-readable error message",
"code": 404,
"details": {
"field": "email",
"reason": "Email already exists"
}
}Validation Error Response:
{
"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:
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
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
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:
// 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:
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:
// 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:
// 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:
// 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:
$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:
- CORS Configuration - Control cross-origin requests
- Authentication - Verify user identity
- Authorization - Control access to resources
- Input Validation - Sanitize and validate user input
- Password Security - Secure password storage
- SQL Injection Prevention - Protect against database attacks
- XSS Prevention - Prevent cross-site scripting
- CSRF Protection - Prevent cross-site request forgery
- Rate Limiting - Prevent abuse and DoS attacks
- 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:
// 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:
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
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=36002. Authentication Middleware
Implement authentication to verify user identity.
JWT Authentication Middleware Example
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
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
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
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
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
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
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
use ElliePHP\Components\Support\Util\Hash;
// Hash password
$hashedPassword = Hash::create($plainPassword);
// Verify password
$isValid = Hash::check($plainPassword, $hashedPassword);Complete Authentication Example
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:
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
// 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
// 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
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
// 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:
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
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
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
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:
- Unit Testing - Test individual classes and methods in isolation
- Integration Testing - Test how components work together
- HTTP Testing - Test controllers and HTTP responses
- Repository Testing - Test data access layer
- Service Testing - Test business logic
- 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 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
# 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:coverageUnit Testing Services
Test business logic in service classes.
Example: UserService Test
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
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
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
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
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:
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
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:
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:
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
// 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
// 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:
// 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:
// 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
public function testGetUserByIdWithNegativeId(): void { }
public function testGetUserByIdWithZeroId(): void { }
public function testGetUserByIdWithVeryLargeId(): void { }8. Use Data Providers for Similar Tests
/**
* @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:
{
"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:
composer test # All tests
composer test:unit # Unit tests only
composer test:integration # Integration tests only
composer test:coverage # Generate HTML coverage reportContinuous Integration
Set up CI/CD to run tests automatically.
Example: GitHub Actions
# .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-textSee 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.
function cache(?string $cacheDriver = null): CacheParameters:
$cacheDriver(string|null): Cache driver name ('file', 'redis', 'sqlite', 'apcu'). UsesCACHE_DRIVERenv if null.
Returns: Cache instance with configured driver
Example:
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.
function config(array|string|null $key = null, mixed $default = null): mixedParameters:
$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:
$appName = config('app.name');
config('app.debug', false);
config(['app.name' => 'MyApp']);See Also: Configuration
container()
Get the container instance or resolve a service.
function container(?string $abstract = null): mixedParameters:
$abstract(string|null): Service identifier to resolve. Returns container if null.
Returns: Container instance or resolved service
Throws: ContainerExceptionInterface, NotFoundExceptionInterface
Example:
$container = container();
$service = container(UserService::class);See Also: Dependency Injection
env()
Get environment variable value or Env instance.
function env(?string $value = null, mixed $defaultValue = null): mixedParameters:
$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:
$debug = env('APP_DEBUG', false);
$host = env('DB_HOST', 'localhost');
$env = env(); // Get Env instanceSee Also: Environment Utilities
report()
Get the Log instance for application logging.
function report(): LogReturns: Logger instance with app and exception channels
Example:
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.
function request(): RequestReturns: PSR-7 ServerRequest instance
Example:
$email = request()->input('email');
$method = request()->method();
$isJson = request()->isJson();See Also: HTTP Request & Response
response()
Create a new HTTP response instance.
function response(int $status = 200): ResponseParameters:
$status(int): HTTP status code (default: 200)
Returns: PSR-7 Response instance
Example:
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.
function app_path(string $path = ""): stringParameters:
$path(string): Optional path to append
Returns: Full path to app directory
Example:
$controllersPath = app_path('Http/Controllers');root_path()
Get the root path of the application.
function root_path(string $path = ""): stringParameters:
$path(string): Optional path to append
Returns: Full path to root directory
Example:
$envPath = root_path('.env');routes_path()
Get the routes path.
function routes_path(string $path = ""): stringParameters:
$path(string): Optional path to append
Returns: Full path to routes directory
Example:
$routerFile = routes_path('router.php');storage_path()
Get the storage path.
function storage_path(string $path = ""): stringParameters:
$path(string): Optional path to append
Returns: Full path to storage directory
Example:
$uploadsPath = storage_path('uploads');storage_cache_path()
Get the storage cache path.
function storage_cache_path(string $path = ""): stringParameters:
$path(string): Optional path to append
Returns: Full path to storage/Cache directory
Example:
$cacheDb = storage_cache_path('cache.db');storage_logs_path()
Get the storage logs path.
function storage_logs_path(string $path = ""): stringParameters:
$path(string): Optional path to append
Returns: Full path to storage/Logs directory
Example:
$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:
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): stringString Operations:
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): stringString Manipulation:
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): stringString Extraction:
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): ?stringPadding & Formatting:
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): stringPattern Matching:
Str::match(string $pattern, string $subject): ?array
Str::matchAll(string $pattern, string $subject): ?arrayValidation:
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): boolUtilities:
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): stringSee Also: String Utilities
File Class
File and directory operations.
File Checks:
File::exists(string $path): bool
File::isFile(string $path): bool
File::isDirectory(string $path): bool
File::isReadable(string $path): bool
File::isWritable(string $path): boolFile Reading:
File::get(string $path): string
File::lines(string $path, bool $skipEmpty = false): array
File::json(string $path, bool $associative = true): mixedFile Writing:
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): intFile Operations:
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): boolFile Information:
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): boolDirectory Operations:
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): boolUtilities:
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): stringSee Also: File Utilities
Json Class
JSON encoding, decoding, and manipulation.
Encoding & Decoding:
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): mixedValidation:
Json::isValid(string $json): bool
Json::validate(string $json, array $schema): bool
Json::lastError(): string
Json::lastErrorCode(): intFile Operations:
Json::fromFile(string $path, bool $associative = true): mixed
Json::toFile(string $path, mixed $value, bool $pretty = false, int $flags = 0): boolManipulation:
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 = '.'): stringDot Notation Access:
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): stringFormatting:
Json::minify(string $json): string
Json::format(string $json): stringConversion:
Json::toXml(string $json, string $rootElement = 'root'): string
Json::toCsv(string $json, bool $includeHeaders = true): stringSee Also: JSON Utilities
Hash Class
Hashing, password hashing, and ID generation.
Password Hashing:
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 = []): stringHash Algorithms:
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): stringFile Hashing:
Hash::file(string $path, string $algorithm = 'sha256', bool $binary = false): string|falseID Generation:
Hash::uuid(): string
Hash::ulid(): string
Hash::nanoid(int $size = 21, ?string $alphabet = null): string
Hash::random(int $length = 32): stringUtilities:
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(): arraySee Also: Hash Utilities
Env Class
Environment variable management with automatic type casting.
Loading:
__construct(string $path, string|array $names = '.env')
load(): self
loadWithRequired(array $required): self
isLoaded(): boolAccess:
get(string $key, mixed $default = null): mixed
has(string $key): bool
all(): arrayValidation:
require(array $variables): self
requireNotEmpty(array $variables): self
requireOneOf(string $variable, array $allowedValues): selfExample:
$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:
Request::fromGlobals(): RequestInput Methods:
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 = []): arrayRequest Information:
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(): boolHeaders:
header(string $name, mixed $default = null): mixed
headers(): array
hasHeader(string $name): bool
bearerToken(): ?stringQuery & Body:
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): boolFiles:
file(string $key): ?UploadedFileInterface
hasFile(string $key): bool
files(): arrayServer & Client:
server(string $key, mixed $default = null): mixed
ip(): string
userAgent(): ?stringSee Also: Request Class
Response Class
PSR-7 Response wrapper with fluent interface.
Factory:
Response::make(int $status = 200): ResponseContent Type Methods:
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): ResponseInterfaceRedirect Methods:
redirect(string $url, int $status = 302): ResponseInterface
redirectPermanent(string $url): ResponseInterface
redirectTemporary(string $url): ResponseInterface
redirectSeeOther(string $url): ResponseInterface
back(string $fallback = '/'): ResponseInterfaceStatus Code Helpers (2xx):
ok(mixed $data = null): ResponseInterface
created(mixed $data = null, ?string $location = null): ResponseInterface
accepted(mixed $data = null): ResponseInterface
noContent(): ResponseInterfaceStatus Code Helpers (4xx):
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): ResponseInterfaceStatus Code Helpers (5xx):
serverError(mixed $data = null): ResponseInterface
serviceUnavailable(mixed $data = null): ResponseInterfaceFile Downloads:
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 = []): ResponseInterfaceHeaders & Cookies:
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): selfResponse Inspection:
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): boolSee 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:
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): boolBatch Operations:
getMultiple(iterable $keys, mixed $default = null): iterable
setMultiple(iterable $values, null|int|DateInterval $ttl = null): bool
deleteMultiple(iterable $keys): boolStatistics:
count(): int
size(): intDriver-Specific Methods:
clearExpired(): bool // File and SQLite drivers onlyExample:
// 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:
CacheFactory::createFileDriver(array $config = []): FileCacheConfig Options:
path(string): Cache directory pathcreate_directory(bool): Auto-create directory (default: true)directory_permissions(int): Directory permissions (default: 0755)
Redis Driver:
CacheFactory::createRedisDriver(array $config = []): RedisCacheConfig Options:
host(string): Redis host (default: '127.0.0.1')port(int): Redis port (default: 6379)password(string|null): Redis passworddatabase(int): Redis database (default: 0)timeout(int): Connection timeout (default: 5)prefix(string): Key prefix (default: 'ellie_cache:')
SQLite Driver:
CacheFactory::createSQLiteDriver(array $config = []): SQLiteCacheConfig Options:
path(string): Database file pathcreate_directory(bool): Auto-create directory (default: true)directory_permissions(int): Directory permissions (default: 0755)
APCu Driver:
CacheFactory::createApcuDriver(array $config = []): ApcuCacheConfig Options:
prefix(string): Key prefix (default: 'ellie_cache:')
Example:
// 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:
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): selfArgument Modes:
InputArgument::REQUIRED: Argument is requiredInputArgument::OPTIONAL: Argument is optionalInputArgument::IS_ARRAY: Argument accepts multiple values
Option Modes:
InputOption::VALUE_NONE: Option doesn't accept a valueInputOption::VALUE_REQUIRED: Option requires a valueInputOption::VALUE_OPTIONAL: Option value is optionalInputOption::VALUE_IS_ARRAY: Option accepts multiple values
Output Methods:
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): voidTable Output:
table(array $headers, array $rows): voidInteractive Methods:
ask(string $question, ?string $default = null): mixed
confirm(string $question, bool $default = true): bool
choice(string $question, array $choices, mixed $default = null): mixedInput Access:
argument(string $name): mixed
option(string $name): mixedExit Codes:
self::SUCCESS = 0
self::FAILURE = 1
self::INVALID = 2Example:
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
php ellie serve [--host=HOST] [--port=PORT] [--docroot=DOCROOT]cache:clear - Clear cache
php ellie cache:clear [--config] [--routes] [--views] [--all]routes - List all registered routes
php ellie routesmake:controller - Generate a controller class
php ellie make:controller ControllerName [--resource] [--api]See Also: Built-in Commands
Additional Resources
- GitHub Repository: https://github.com/elliephp/framework
- Issue Tracker: https://github.com/elliephp/framework/issues
- PSR-7 (HTTP Message Interface): https://www.php-fig.org/psr/psr-7/
- PSR-11 (Container Interface): https://www.php-fig.org/psr/psr-11/
- PSR-15 (HTTP Handlers): https://www.php-fig.org/psr/psr-15/
- PSR-16 (Simple Cache): https://www.php-fig.org/psr/psr-16/
- PHP-DI Documentation: https://php-di.org/
- Monolog Documentation: https://github.com/Seldaek/monolog
Copyright © 2024 ElliePHP Framework. Released under the MIT License.