Mini Framework - Common Patterns
This document contains common patterns for extending and customizing Mini framework behavior.
Table of Contents
- Overriding Framework Services
- Request Processing (Middleware Pattern)
- Response Processing (Output Buffering)
Overriding Framework Services
Mini allows applications to override framework default services using config files.
How It Works
The framework registers services using Mini::$mini->loadServiceConfig(), which:
- First checks for application config at
_config/[namespace]/[ClassName].php - Falls back to framework default at
vendor/fubber/mini/config/[namespace]/[ClassName].php
This means you override services by creating config files, not by registering them before the framework loads.
Pattern: Override Services via Config Files
Example: Custom Logger (Monolog)
<?php
// _config/Psr/Log/LoggerInterface.php
return new \Monolog\Logger('app', [
new \Monolog\Handler\StreamHandler('php://stderr', \Monolog\Logger::DEBUG),
new \Monolog\Handler\SentryHandler(/* ... */),
]);
Example: Custom Cache (Redis)
<?php
// _config/Psr/SimpleCache/CacheInterface.php
return new \Symfony\Component\Cache\Psr16Cache(
new \Symfony\Component\Cache\Adapter\RedisAdapter(
\Symfony\Component\Cache\Adapter\RedisAdapter::createConnection('redis://localhost')
)
);
Example: Custom Database (PostgreSQL)
<?php
// _config/PDO.php
return new PDO(
'pgsql:host=db.example.com;dbname=myapp',
'user',
'pass',
[
PDO::ATTR_TIMEOUT => 5,
PDO::ATTR_PERSISTENT => true,
]
);
Example: Custom UUID Factory
<?php
// _config/mini/UUID/FactoryInterface.php
return new \mini\UUID\UUID4Factory(); // Use v4 instead of v7
Config File Lookup
Framework services use this pattern:
Mini::$mini->addService(
LoggerInterface::class,
Lifetime::Singleton,
fn() => Mini::$mini->loadServiceConfig(LoggerInterface::class)
);
When you call log(), Mini looks for config in this order:
_config/Psr/Log/LoggerInterface.php(your override)vendor/fubber/mini/config/Psr/Log/LoggerInterface.php(framework default)
Note: You cannot override framework services by registering them in app/bootstrap.php before the framework loads. The framework unconditionally registers its services, and loadServiceConfig() handles the override logic via config files.
Request Processing (Middleware Pattern)
Mini doesn't have PSR-15 middleware, but phase lifecycle hooks provide the same capabilities using standard PHP patterns.
Pattern: Use Phase Transition Hooks
Phase hooks fire when the application transitions to Ready phase (when mini\bootstrap() is called). Use onEnteringState() for "before request" logic and onEnteredState() for "after bootstrap" logic.
When hooks fire:
onEnteringState(Phase::Ready)- Before Ready phase transition (before error handlers, output buffering)onEnteredState(Phase::Ready)- After Ready phase entered (after bootstrap completes)
Example: Authentication Middleware
<?php
// app/bootstrap.php
use mini\Mini;
use mini\Phase;
// Register authentication check for protected routes
// Fires when entering Ready phase (before error handlers set up)
Mini::$mini->phase->onEnteringState(Phase::Ready, function() {
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
// Define protected paths
$protectedPaths = ['/admin', '/api', '/profile'];
// Check if current path is protected
foreach ($protectedPaths as $protected) {
if (str_starts_with($path, $protected)) {
// Check authentication
session_start();
if (!isset($_SESSION['user_id'])) {
// Not authenticated - return 401
http_response_code(401);
header('Content-Type: application/json');
echo json_encode(['error' => 'Authentication required']);
exit;
}
break;
}
}
});
Example: Request Logging
<?php
// app/bootstrap.php
use mini\Mini;
use mini\Phase;
Mini::$mini->phase->onEnteringState(Phase::Ready, function() {
$start = microtime(true);
// Log request start
error_log(sprintf(
'[%s] %s %s',
date('Y-m-d H:i:s'),
$_SERVER['REQUEST_METHOD'] ?? 'GET',
$_SERVER['REQUEST_URI'] ?? '/'
));
// Register shutdown function to log duration
register_shutdown_function(function() use ($start) {
$duration = microtime(true) - $start;
error_log(sprintf('Request completed in %.3f seconds', $duration));
});
});
Example: CORS Headers
<?php
// app/bootstrap.php
use mini\Mini;
use mini\Phase;
Mini::$mini->phase->onEnteringState(Phase::Ready, function() {
// Add CORS headers for API routes
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
if (str_starts_with($path, '/api/')) {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
// Handle preflight requests
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
}
});
Example: Request Body Parsing (Extended)
<?php
// app/bootstrap.php
use mini\Mini;
use mini\Phase;
Mini::$mini->phase->onEnteringState(Phase::Ready, function() {
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
// Parse XML request bodies
if (str_contains($contentType, 'application/xml')) {
$xml = file_get_contents('php://input');
try {
$data = simplexml_load_string($xml);
$_POST = json_decode(json_encode($data), true);
} catch (\Exception $e) {
http_response_code(400);
echo json_encode(['error' => 'Invalid XML']);
exit;
}
}
// Parse form-data with file uploads
if (str_contains($contentType, 'multipart/form-data')) {
// PHP handles this automatically, but you could add validation here
$maxFileSize = 10 * 1024 * 1024; // 10MB
foreach ($_FILES as $file) {
if ($file['size'] > $maxFileSize) {
http_response_code(413);
echo json_encode(['error' => 'File too large']);
exit;
}
}
}
});
Multiple Hooks = Multiple Middleware
You can register multiple listeners to chain middleware-like logic:
<?php
// app/bootstrap.php
use mini\Mini;
use mini\Phase;
// Middleware 1: Rate limiting
Mini::$mini->phase->onEnteringState(Phase::Ready, function() {
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$cache = \mini\cache('rate-limit');
$key = "rate_limit:{$ip}";
$requests = $cache->get($key, 0);
if ($requests > 100) {
http_response_code(429);
echo json_encode(['error' => 'Too many requests']);
exit;
}
$cache->set($key, $requests + 1, 60); // 100 requests per minute
});
// Middleware 2: Request ID
Mini::$mini->phase->onEnteringState(Phase::Ready, function() {
$requestId = bin2hex(random_bytes(8));
$_SERVER['HTTP_X_REQUEST_ID'] = $requestId;
header("X-Request-ID: {$requestId}");
});
// Middleware 3: Security headers
Mini::$mini->phase->onEnteringState(Phase::Ready, function() {
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
});
Response Processing (Output Buffering)
The onEnteredState(Phase::Ready) hook fires after the Ready phase is entered, which means:
- Error handlers are configured
- Output buffering has started
- Application is ready to handle requests
- All framework features are available
This is the perfect place for "after bootstrap" processing using PHP's output buffering.
Pattern: Custom Output Handler
Register a custom output buffer handler after entering Ready phase:
Example: HTML Minification
<?php
// app/bootstrap.php
use mini\Mini;
use mini\Phase;
Mini::$mini->phase->onEnteredState(Phase::Ready, function() {
// Start a new output buffer with custom handler
ob_start(function($buffer) {
// Only minify HTML responses
$contentType = '';
foreach (headers_list() as $header) {
if (stripos($header, 'Content-Type:') === 0) {
$contentType = $header;
break;
}
}
if (str_contains($contentType, 'text/html')) {
// Simple HTML minification
$buffer = preg_replace('/\s+/', ' ', $buffer); // Collapse whitespace
$buffer = preg_replace('/>\s+</', '><', $buffer); // Remove space between tags
}
return $buffer;
});
});
Example: Response Compression
<?php
// app/bootstrap.php
use mini\Mini;
use mini\Phase;
Mini::$mini->phase->onEnteredState(Phase::Ready, function() {
// Use PHP's built-in compression handler
if (extension_loaded('zlib')) {
ob_start('ob_gzhandler');
}
});
Example: Response Time Header
<?php
// app/bootstrap.php
use mini\Mini;
use mini\Phase;
Mini::$mini->phase->onEnteredState(Phase::Ready, function() {
$start = microtime(true);
ob_start(function($buffer) use ($start) {
$duration = microtime(true) - $start;
header("X-Response-Time: " . round($duration * 1000, 2) . "ms");
return $buffer;
});
});
Example: Inject Analytics Script
<?php
// app/bootstrap.php
use mini\Mini;
use mini\Phase;
Mini::$mini->phase->onEnteredState(Phase::Ready, function() {
ob_start(function($buffer) {
// Only inject into HTML responses
$contentType = '';
foreach (headers_list() as $header) {
if (stripos($header, 'Content-Type:') === 0) {
$contentType = $header;
break;
}
}
if (str_contains($contentType, 'text/html') && str_contains($buffer, '</body>')) {
$analytics = '<script>/* Google Analytics code here */</script>';
$buffer = str_replace('</body>', $analytics . '</body>', $buffer);
}
return $buffer;
});
});
Example: Security Headers Injection
<?php
// app/bootstrap.php
use mini\Mini;
use mini\Phase;
Mini::$mini->phase->onEnteredState(Phase::Ready, function() {
ob_start(function($buffer) {
// Inject Content-Security-Policy for HTML pages
$contentType = '';
foreach (headers_list() as $header) {
if (stripos($header, 'Content-Type:') === 0) {
$contentType = $header;
break;
}
}
if (str_contains($contentType, 'text/html')) {
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
}
return $buffer;
});
});
Manipulating Response Headers
Output buffer handlers can manipulate headers before they're sent:
<?php
// app/bootstrap.php
use mini\Mini;
use mini\Phase;
Mini::$mini->phase->onEnteredState(Phase::Ready, function() {
ob_start(function($buffer) {
// Add cache headers based on content
if (strlen($buffer) > 1024 && !str_contains($buffer, 'user-specific')) {
header('Cache-Control: public, max-age=3600');
header('ETag: "' . md5($buffer) . '"');
} else {
header('Cache-Control: no-cache, no-store, must-revalidate');
}
// Add content length
header('Content-Length: ' . strlen($buffer));
return $buffer;
});
});
Stacking Output Handlers
Multiple output handlers create a processing chain:
<?php
// app/bootstrap.php
use mini\Mini;
use mini\Phase;
Mini::$mini->phase->onEnteredState(Phase::Ready, function() {
// Handler 1: Minify HTML
ob_start(function($buffer) {
if (str_contains(headers_list()[0] ?? '', 'text/html')) {
$buffer = preg_replace('/\s+/', ' ', $buffer);
}
return $buffer;
});
// Handler 2: Compress
ob_start('ob_gzhandler');
// Handler 3: Add headers
ob_start(function($buffer) {
header('X-Powered-By: Mini Framework');
header('X-Content-Length: ' . strlen($buffer));
return $buffer;
});
});
The handlers execute in reverse order (Handler 3 → Handler 2 → Handler 1).
Complete Example: Full Request/Response Pipeline
Here's a complete example combining all patterns:
<?php
// app/bootstrap.php
use mini\Mini;
use mini\Phase;
use mini\Lifetime;
// Override services via config files (see _config/Psr/Log/LoggerInterface.php)
// NOT done here - use config files instead
// Request processing - fires when entering Ready phase
Mini::$mini->phase->onEnteringState(Phase::Ready, function() {
// Rate limiting
// CORS headers
// Authentication
// Request logging
});
// Response processing - fires after Ready phase entered
Mini::$mini->phase->onEnteredState(Phase::Ready, function() {
$start = microtime(true);
ob_start(function($buffer) use ($start) {
// Minify HTML
if (str_contains(headers_list()[0] ?? '', 'text/html')) {
$buffer = preg_replace('/\s+/', ' ', $buffer);
}
// Add performance headers
$duration = microtime(true) - $start;
header("X-Response-Time: " . round($duration * 1000, 2) . "ms");
header("Content-Length: " . strlen($buffer));
return $buffer;
});
// Compression
if (extension_loaded('zlib')) {
ob_start('ob_gzhandler');
}
});
Best Practices
Service Overrides
- ✅ Create config files in
_config/[namespace]/[ClassName].php - ✅ Return properly configured service instances from config files
- ✅ Implement required PSR interfaces when replacing framework services (PSR-3, PSR-16, etc.)
- ❌ Don't try to override by registering services in
app/bootstrap.php(won't work)
Phase Hooks for Request Processing
- ✅ Use
onEnteringState(Phase::Ready)for authentication, logging, headers - ✅ Call
exitto short-circuit request processing - ✅ Set headers and status codes directly with
header()andhttp_response_code() - ❌ Don't try to access Scoped services (db(), auth()) - not available until Ready phase entered
Phase Hooks for Response Processing
- ✅ Use
onEnteredState(Phase::Ready)withob_start()for response processing - ✅ Check
headers_list()to determine content type - ✅ Use
header()to add/modify response headers - ✅ Return the (possibly modified) buffer from handler
- ❌ Don't call
exitin output handlers (breaks buffer chain)
See Also
- CLAUDE.md - Full framework documentation
- README.md - Getting started guide
- REFERENCE.md - API reference