mini\Converter
namespace
Converter - Type Conversion System
Philosophy
Mini provides automatic type conversion with reflection-based type matching. Transform route return values, exceptions, and domain objects to HTTP responses without manual serialization code in every route handler.
Key Principles:
- Type-safe - Uses PHP's type system via reflection
- Composable - Register converters for any input→output type combination
- Union input types - Single converter handles multiple input types
- Specificity resolution - Most specific converter wins (single > union, class > interface > parent)
- Extensible - Applications can register custom converters
- Fail-fast - Throws if no converter found (catches bugs early)
Setup
No configuration needed! The converter registry works out of the box:
use mini\Mini;
use mini\Converter\ConverterRegistryInterface;
// Get the registry (automatically created as singleton)
$registry = Mini::$mini->get(ConverterRegistryInterface::class);
// Use convert() helper function
$response = convert($value, ResponseInterface::class);
// Throws RuntimeException if no converter registered
// Check if converter exists before converting
if ($registry->has($value, ResponseInterface::class)) {
$response = $registry->convert($value, ResponseInterface::class);
}
Registering Custom Converters
Register converters in bootstrap.php, not in config files:
<?php
// bootstrap.php
use mini\Mini;
use mini\Converter\ConverterRegistryInterface;
use Psr\Http\Message\ResponseInterface;
use mini\Http\Message\Response;
// Get the converter registry
$registry = Mini::$mini->get(ConverterRegistryInterface::class);
// Register application-specific converters
$registry->register(function(MyModel $model): ResponseInterface {
$json = json_encode($model->toArray());
return new Response(200, ['Content-Type' => 'application/json'], $json);
});
Config vs Bootstrap:
- Config (
_config/mini/Converter/ConverterRegistryInterface.php) - Factory for creating registry instance - Bootstrap (
bootstrap.php) - Where you register application-specific converters
Custom Registry Implementation
Override the registry implementation in config only if you need a custom registry class:
<?php
// _config/mini/Converter/ConverterRegistryInterface.php
use mini\Converter\ConverterRegistry;
// Custom registry with logging
return new class extends ConverterRegistry {
public function convert(mixed $input, string $targetType): mixed {
error_log("Converting " . get_debug_type($input) . " to $targetType");
return parent::convert($input, $targetType);
}
};
Common Usage Examples
Basic Conversion
// String to response
$response = convert("Hello", ResponseInterface::class);
// → 200 text/plain response with "Hello"
// Array to response
$response = convert(['users' => $users], ResponseInterface::class);
// → 200 application/json response
// Exception to response
$response = convert(new \RuntimeException('Error'), ResponseInterface::class);
// → 500 error page response
Route Return Value Conversion
Routes can return any value - converters transform it to a PSR-7 Response:
// _routes/api/users.php
return ['users' => db()->query("SELECT * FROM users")->toArray()];
// Automatically converted to JSON response
// _routes/ping.php
return "pong";
// Automatically converted to text/plain response
// _routes/profile.php
$user = db()->queryOne("SELECT * FROM users WHERE id = ?", [$_GET['id']]);
return $user ?: throw new NotFoundException('User not found');
// Array converted to JSON, or exception converted to 404 error page
Exception to HTTP Response
Exceptions are automatically converted to appropriate error pages:
throw new \RuntimeException('Something went wrong');
// Converter transforms to 500 error page response
// In debug mode, exception message is shown
// In production, generic "Internal Server Error" message
Registering Converters
All converter registration happens in bootstrap.php:
<?php
// bootstrap.php
use mini\Mini;
use mini\Converter\ConverterRegistryInterface;
$registry = Mini::$mini->get(ConverterRegistryInterface::class);
// Now register your converters...
Simple Converter
// bootstrap.php
$registry->register(function(string $text): ResponseInterface {
return new Response(200, ['Content-Type' => 'text/plain'], $text);
});
Union Input Types
Handle multiple input types with a single converter:
// bootstrap.php
$registry->register(function(string|array $data): ResponseInterface {
if (is_string($data)) {
return new Response(200, ['Content-Type' => 'text/plain'], $data);
}
$json = json_encode($data);
return new Response(200, ['Content-Type' => 'application/json'], $json);
});
Content Negotiation
Single converter handles different output formats based on request:
// bootstrap.php
use function mini\request;
$registry->register(function(array $data): ResponseInterface {
$accept = request()->getHeaderLine('Accept');
if (str_contains($accept, 'application/json')) {
$json = json_encode($data);
return new Response(200, ['Content-Type' => 'application/json'], $json);
}
$html = render('data-view', ['data' => $data]);
return new Response(200, ['Content-Type' => 'text/html'], $html);
});
Custom Domain Objects
// bootstrap.php
interface Jsonable {
public function toJson(): string;
}
// Any Jsonable object → JSON response
$registry->register(function(Jsonable $obj): ResponseInterface {
return new Response(
200,
['Content-Type' => 'application/json'],
$obj->toJson()
);
});
// Usage in routes
class User implements Jsonable {
public function toJson(): string {
return json_encode(['id' => $this->id, 'name' => $this->name]);
}
}
// _routes/api/user.php
$user = new User();
return $user; // Automatically converted to JSON response
Advanced Features
Converter Resolution Order
The registry finds the most specific converter:
- Direct single-type converter (most specific)
- Union type converter (less specific)
- Parent class converters (inheritance)
- Interface converters (implemented interfaces)
// Register converters
$registry->register(function(\Exception $e): ResponseInterface { /* ... */ });
$registry->register(function(\Throwable $e): ResponseInterface { /* ... */ });
// Convert RuntimeException (extends Exception implements Throwable)
$response = convert(new \RuntimeException(), ResponseInterface::class);
// Uses Exception converter (most specific class match)
Union Type Specificity
Single-type converters override union members:
// Register union converter
$registry->register(function(string|int $data): ResponseInterface {
return new Response(200, [], (string)$data);
});
// Register more specific single-type converter (overrides union member)
$registry->register(function(string $text): ResponseInterface {
return new Response(200, ['Content-Type' => 'text/plain'], $text);
});
// Convert string
$response = convert("hello", ResponseInterface::class);
// Uses string converter (more specific than union)
// Convert int
$response = convert(42, ResponseInterface::class);
// Uses union converter (no int-specific converter)
Conflict Detection
The registry prevents ambiguous registrations:
// Register union converter
$registry->register(function(string|int $data): ResponseInterface { /* ... */ });
// This will throw InvalidArgumentException (conflict)
$registry->register(function(int|bool $data): ResponseInterface { /* ... */ });
// Error: int already part of another union
// This is OK (single-type overrides union member)
$registry->register(function(string $text): ResponseInterface { /* ... */ });
Replacing Converters
Use replace() to override existing converters without throwing conflicts:
// Override the default string→Response converter
$registry->replace(function(string $text): ResponseInterface {
return new Response(200, ['Content-Type' => 'text/html'], "<p>$text</p>");
});
Named Targets
Register converters with custom target names (bypasses return type validation):
// Convert BackedEnum to SQL value
$registry->register(fn(\BackedEnum $e) => $e->value, 'sql-value');
// Later, use with the named target
$value = $registry->convert($myEnum, 'sql-value');
Interface & Implementation
ConverterInterface
interface ConverterInterface
{
/**
* Get the input type this converter accepts
*
* May be a single type or union type string (e.g., "string|array|int").
*/
public function getInputType(): string;
/**
* Get the output type this converter produces
*/
public function getOutputType(): string;
/**
* Check if this converter can handle the given input for target type
*/
public function supports(mixed $input, string $targetType): bool;
/**
* Convert the input to the target type
*/
public function convert(mixed $input, string $targetType): mixed;
}
ClosureConverter
Wraps typed closures and uses reflection to extract types:
$converter = new ClosureConverter(
function(string $text): ResponseInterface {
return new Response(200, [], $text);
}
);
$converter->getInputType(); // "string"
$converter->getOutputType(); // "Psr\Http\Message\ResponseInterface"
Requirements:
- Exactly one typed parameter
- Typed return value (non-nullable)
- No nullable input types (
?string,mixednot allowed) - Union input types allowed (
string|int) - Union output types not allowed
ConverterRegistry API
$registry = new ConverterRegistry();
// Register converter (throws if duplicate)
$registry->register($converter); // ConverterInterface
$registry->register($closure); // \Closure (wrapped in ClosureConverter)
// Replace existing converter (allows override)
$registry->replace($closure); // Overwrites existing without conflict
// Named target (bypasses return type validation)
$registry->register(fn(\BackedEnum $e) => $e->value, 'sql-value');
// Check if converter exists
$has = $registry->has($input, ResponseInterface::class);
// Get converter
$converter = $registry->get($input, ResponseInterface::class);
// Convert value
$response = $registry->convert($input, ResponseInterface::class);
// Returns null if no converter found
Practical Examples
API Error Handling
// bootstrap.php
// Define custom exception classes
class ValidationException extends \Exception {
public function __construct(public array $errors) {
parent::__construct('Validation failed');
}
}
class NotFoundException extends \Exception {
public function __construct(string $message = 'Not found') {
parent::__construct($message);
}
}
// Get registry and register converters for specific exceptions
$registry = Mini::$mini->get(ConverterRegistryInterface::class);
$registry->register(function(ValidationException $e): ResponseInterface {
$json = json_encode(['errors' => $e->errors]);
return new Response(400, ['Content-Type' => 'application/json'], $json);
});
$registry->register(function(NotFoundException $e): ResponseInterface {
$body = render('404', ['message' => $e->getMessage()]);
return new Response(404, ['Content-Type' => 'text/html'], $body);
});
// Usage in routes
// _routes/api/users.php
$user = db()->queryOne("SELECT * FROM users WHERE id = ?", [$_GET['id']]);
if (!$user) {
throw new NotFoundException('User not found');
}
return $user;
Domain Model Serialization
// bootstrap.php
// Domain models
class Product {
public function __construct(
public int $id,
public string $name,
public float $price
) {}
}
class ProductCollection {
public function __construct(public array $products) {}
}
// Get registry and register converters
$registry = Mini::$mini->get(ConverterRegistryInterface::class);
$registry->register(function(Product $product): ResponseInterface {
$json = json_encode([
'id' => $product->id,
'name' => $product->name,
'price' => $product->price,
]);
return new Response(200, ['Content-Type' => 'application/json'], $json);
});
$registry->register(function(ProductCollection $collection): ResponseInterface {
$json = json_encode([
'products' => array_map(
fn($p) => ['id' => $p->id, 'name' => $p->name, 'price' => $p->price],
$collection->products
)
]);
return new Response(200, ['Content-Type' => 'application/json'], $json);
});
// Routes return domain objects
// _routes/products.php
$products = array_map(
fn($row) => new Product($row['id'], $row['name'], $row['price']),
db()->query("SELECT * FROM products")->toArray()
);
return new ProductCollection($products);
Multi-Format API
// bootstrap.php
$registry = Mini::$mini->get(ConverterRegistryInterface::class);
// Accept header-based content negotiation
$registry->register(function(array $data): ResponseInterface {
$accept = request()->getHeaderLine('Accept');
// JSON (API clients)
if (str_contains($accept, 'application/json')) {
$json = json_encode($data);
return new Response(200, ['Content-Type' => 'application/json'], $json);
}
// XML (legacy clients)
if (str_contains($accept, 'application/xml')) {
$xml = arrayToXml($data);
return new Response(200, ['Content-Type' => 'application/xml'], $xml);
}
// HTML (browsers)
$html = render('data-view', ['data' => $data]);
return new Response(200, ['Content-Type' => 'text/html'], $html);
});
// Single route handler supports all formats
// _routes/api/users.php
return ['users' => db()->query("SELECT * FROM users")->toArray()];
// Returns JSON, XML, or HTML based on Accept header
Best Practices
- Register converters in bootstrap.php - Not in config files
- Config is for factory only - Use
_config/mini/Converter/ConverterRegistryInterface.phponly to customize the registry implementation - Single responsibility - One converter per input→output type combination
- Use union types wisely - Only when conversion logic is truly shared
- Leverage specificity - Register general converters, override with specific ones
- Handle null gracefully - Remember
convert()returns null when no converter found - Type-first design - Let type system guide converter selection
- Content negotiation - Use Accept header for format selection
Performance
- Fast lookup - O(1) for direct type match, O(n) for type hierarchy walk
- No overhead - Reflection only at registration time, not during conversion
- Efficient storage - Converters indexed by target type, then input type
- Lazy evaluation - Only walks type hierarchy until first match found
Configuration
Config File (Factory):
_config/mini/Converter/ConverterRegistryInterface.php(optional) - Creates the registry instance- Framework provides default
ConverterRegistryimplementation - Override only if you need a custom registry class
Bootstrap File (Registration):
bootstrap.php(recommended) - Where you register application-specific converters
Service Registration:
ConverterRegistryInterface- Singleton (shared converter registry across application)
Overriding the Registry Implementation
Only override the config file if you need to customize the registry class itself:
<?php
// _config/mini/Converter/ConverterRegistryInterface.php
use mini\Converter\ConverterRegistry;
// Create custom registry with additional logging
return new class extends ConverterRegistry {
public function convert(mixed $input, string $targetType): mixed
{
error_log("Converting " . get_debug_type($input) . " to $targetType");
return parent::convert($input, $targetType);
}
};
For normal converter registration, use bootstrap.php instead.
Future Possibilities
Beyond HTTP responses, the converter system can handle other transformations:
- CLI responses -
Throwable → CliResponse - Serialization -
DomainModel → array - Deserialization -
array → DomainModel - Content transformation -
MarkdownString → HtmlString - Format conversion -
Image → Thumbnail
The type-based approach works for any input→output conversion scenario.