Dehydrator.php
PHP
Path: src/Database/Dehydrator.php
<?php
namespace mini\Database;
use mini\Converter\ConverterRegistryInterface;
use mini\Mini;
/**
* Converts between entity objects and SQL-compatible arrays
*
* Handles hydration/dehydration via:
* 1. Hydration interface - if entity implements Hydration, uses fromSqlRow()/toSqlRow()
* 2. Reflection fallback - maps properties to columns with type conversion
*
* Used by PartialQuery for reading and by write operations for validation.
*/
final class Dehydrator
{
/**
* Hydrate an array to an entity instance
*
* @template T of object
* @param array<string, mixed> $row Associative array of column => value
* @param class-string<T> $entityClass The entity class to hydrate into
* @param array|false $constructorArgs Constructor args, or false to skip constructor
* @return T
*/
public static function hydrate(array $row, string $entityClass, array|false $constructorArgs = false): object
{
// If entity implements Hydration, use its fromSqlRow() method
if (is_subclass_of($entityClass, Hydration::class)) {
return $entityClass::fromSqlRow($row);
}
// Reflection-based hydration
return self::hydrateViaReflection($row, $entityClass, $constructorArgs);
}
/**
* Hydrate using reflection (maps columns to properties with type conversion)
*
* @template T of object
* @param array<string, mixed> $row
* @param class-string<T> $entityClass
* @param array|false $constructorArgs
* @return T
*/
private static function hydrateViaReflection(array $row, string $entityClass, array|false $constructorArgs): object
{
$refClass = new \ReflectionClass($entityClass);
$converterRegistry = null;
// Create instance with or without constructor
if ($constructorArgs === false) {
$entity = $refClass->newInstanceWithoutConstructor();
} else {
$entity = $refClass->newInstanceArgs($constructorArgs);
}
// Map columns to properties
foreach ($row as $propertyName => $value) {
if (!$refClass->hasProperty($propertyName)) {
continue; // Unknown column, skip
}
$prop = $refClass->getProperty($propertyName);
// Get target type for conversion
$refType = $prop->getType();
$targetType = null;
if ($refType instanceof \ReflectionNamedType && !$refType->isBuiltin()) {
$targetType = $refType->getName();
}
// Convert value if target is a class and value needs conversion
if ($value !== null && $targetType !== null && !($value instanceof $targetType)) {
if ($converterRegistry === null) {
$converterRegistry = Mini::$mini->get(ConverterRegistryInterface::class);
}
$found = false;
$converted = $converterRegistry->tryConvert($value, $targetType, 'sql-value', $found);
if ($found) {
$value = $converted;
}
}
$prop->setValue($entity, $value);
}
return $entity;
}
/**
* Dehydrate an entity to a SQL-compatible array
*
* @param object $entity The entity to dehydrate
* @return array<string, mixed> Associative array of column => value
*/
public static function dehydrate(object $entity): array
{
// If entity implements Hydration, use its toSqlRow() method
if ($entity instanceof Hydration) {
return $entity->toSqlRow();
}
// Reflection-based dehydration
return self::dehydrateViaReflection($entity);
}
/**
* Dehydrate using reflection (reads public properties, converts values)
*/
private static function dehydrateViaReflection(object $entity): array
{
$data = [];
$converterRegistry = null;
$refClass = new \ReflectionClass($entity);
foreach ($refClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) {
// Skip static properties
if ($prop->isStatic()) {
continue;
}
$name = $prop->getName();
// Skip uninitialized properties
if (!$prop->isInitialized($entity)) {
continue;
}
$value = $prop->getValue($entity);
// Convert non-scalar values to SQL-compatible format
if ($value !== null && !is_scalar($value)) {
// Lazy-load converter registry
if ($converterRegistry === null) {
$converterRegistry = Mini::$mini->get(ConverterRegistryInterface::class);
}
// Try to convert to sql-value
$found = false;
$converted = $converterRegistry->tryConvert($value, 'sql-value', null, $found);
if ($found) {
$value = $converted;
} elseif ($value instanceof SqlValue) {
// Direct SqlValue support
$value = $value->toSqlValue();
} elseif ($value instanceof \DateTimeInterface) {
// Common case: DateTime to string
$value = $value->format('Y-m-d H:i:s');
} elseif ($value instanceof \BackedEnum) {
// Backed enums to their value
$value = $value->value;
} elseif ($value instanceof \UnitEnum) {
// Unit enums to their name
$value = $value->name;
} elseif ($value instanceof \Stringable) {
// Stringable objects
$value = (string) $value;
} elseif (is_object($value) || is_array($value)) {
// Last resort: JSON encode complex structures
$value = json_encode($value, JSON_THROW_ON_ERROR);
}
}
$data[$name] = $value;
}
return $data;
}
}