ConverterRegistryInterface.php

PHP

Path: src/Converter/ConverterRegistryInterface.php

<?php

namespace mini\Converter;

/**
 * Interface for converter registry implementations
 *
 * The converter registry manages type converters that transform values from
 * one type to another. The primary use case is converting route return values
 * and exceptions to HTTP responses, but the system is general-purpose.
 *
 * Applications register converters during bootstrap:
 * ```php
 * // bootstrap.php
 * use mini\Mini;
 * use mini\Converter\ConverterRegistryInterface;
 *
 * $registry = Mini::$mini->get(ConverterRegistryInterface::class);
 *
 * // Register custom converter
 * $registry->register(function(MyModel $model): ResponseInterface {
 *     return new Response(200, ['Content-Type' => 'application/json'], $model->toJson());
 * });
 * ```
 *
 * The registry provides:
 * - Type-safe converter registration via typed closures
 * - Union input type support (single converter handles multiple types)
 * - Specificity resolution (single > union, class > interface > parent)
 * - Conflict detection for overlapping registrations
 *
 * @see ConverterInterface For implementing custom converter classes
 * @see ClosureConverter For closure-based converters (automatic wrapping)
 */
interface ConverterRegistryInterface
{
    /**
     * Register a converter
     *
     * Accepts either a ConverterInterface implementation or a typed closure.
     * Closures are automatically wrapped in ClosureConverter.
     *
     * Closure requirements:
     * - Exactly one typed parameter (may be union type: string|array)
     * - Typed return value (single type only, no unions or null) - unless $targetName is specified
     * - No null in input or output types
     *
     * Examples:
     * ```php
     * // Simple converter
     * $registry->register(function(string $text): ResponseInterface {
     *     return new Response(200, ['Content-Type' => 'text/plain'], $text);
     * });
     *
     * // Union input type
     * $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);
     * });
     *
     * // Named target (bypasses return type validation for closures)
     * $registry->register(fn(\BackedEnum $e) => $e->value, 'sql-value');
     *
     * // Named source and target (for bidirectional conversions like database values)
     * $registry->register(fn(string $s): \DateTimeImmutable => new \DateTimeImmutable($s), null, 'sql-value');
     * ```
     *
     * @param ConverterInterface|\Closure $converter Converter instance or typed closure
     * @param ?string $targetName Optional explicit target name (bypasses return type validation for closures)
     * @param ?string $sourceName Optional explicit source name (for named source types like 'sql-value')
     * @throws \InvalidArgumentException If converter conflicts with existing registration
     * @throws \InvalidArgumentException If closure signature is invalid
     */
    public function register(ConverterInterface|\Closure $converter, ?string $targetName = null, ?string $sourceName = null): void;

    /**
     * Replace an existing converter
     *
     * Similar to register() but allows overriding existing converters without
     * throwing conflicts. Useful for customizing default framework converters.
     *
     * If no converter exists for the given input→output type combination,
     * behaves identically to register().
     *
     * When replacing a union converter, all member type aliases are updated
     * to point to the new converter.
     *
     * Example:
     * ```php
     * // Override the default string→Response converter
     * $registry->replace(function(string $text): ResponseInterface {
     *     return new Response(200, ['Content-Type' => 'text/html'], "<p>$text</p>");
     * });
     * ```
     *
     * @param ConverterInterface|\Closure $converter Converter instance or typed closure
     * @param ?string $targetName Optional explicit target name (bypasses return type validation for closures)
     * @param ?string $sourceName Optional explicit source name (for named source types like 'sql-value')
     * @throws \InvalidArgumentException If closure signature is invalid
     */
    public function replace(ConverterInterface|\Closure $converter, ?string $targetName = null, ?string $sourceName = null): void;

    /**
     * Check if a converter exists for input to target type
     *
     * @param mixed $input The value to convert
     * @param class-string $targetType The desired output type
     * @param ?string $sourceType Optional named source type (e.g., 'sql-value') instead of inferring from $input
     * @return bool True if a suitable converter exists
     */
    public function has(mixed $input, string $targetType, ?string $sourceType = null): bool;

    /**
     * Get the converter for input to target type
     *
     * Returns the most specific converter based on type hierarchy:
     * 1. Direct single-type converter (most specific)
     * 2. Union type converter (less specific)
     * 3. Parent class converters
     * 4. Interface converters
     *
     * @param mixed $input The value to convert
     * @param class-string $targetType The desired output type
     * @param ?string $sourceType Optional named source type (e.g., 'sql-value') instead of inferring from $input
     * @return ConverterInterface|null The converter, or null if none found
     */
    public function get(mixed $input, string $targetType, ?string $sourceType = null): ?ConverterInterface;

    /**
     * Convert a value to target type
     *
     * Finds the most specific converter and performs the conversion.
     * Throws if no suitable converter is found.
     *
     * Use has() to check if a converter exists before calling convert():
     * ```php
     * if ($registry->has($value, ResponseInterface::class)) {
     *     $response = $registry->convert($value, ResponseInterface::class);
     * }
     *
     * // With named source type for database hydration
     * $datetime = $registry->convert($dbString, \DateTimeImmutable::class, 'sql-value');
     * ```
     *
     * @template O
     * @param mixed $input The value to convert
     * @param class-string<O> $targetType The desired output type
     * @param ?string $sourceType Optional named source type (e.g., 'sql-value') instead of inferring from $input
     * @return O The converted value
     * @throws \RuntimeException If no converter is registered for this input→target combination
     */
    public function convert(mixed $input, string $targetType, ?string $sourceType = null): mixed;

    /**
     * Try to convert a value, returning null if no converter handles it
     *
     * Unlike convert(), this method doesn't throw. Use the $found out-parameter
     * to distinguish between "converted to null" and "no converter found".
     *
     * This method also checks fallback handlers, which has() does not.
     *
     * ```php
     * $result = $registry->tryConvert($value, SomeEnum::class, 'sql-value', $found);
     * if ($found) {
     *     // $result is the converted value (may be null if converter returned null)
     * }
     * ```
     *
     * @template O
     * @param mixed $input The value to convert
     * @param class-string<O> $targetType The desired output type
     * @param ?string $sourceType Optional named source type (e.g., 'sql-value') instead of inferring from $input
     * @param bool $found Set to true if conversion succeeded, false otherwise
     * @return O|null The converted value, or null if no converter found
     */
    public function tryConvert(mixed $input, string $targetType, ?string $sourceType = null, bool &$found = false): mixed;
}