functions.php
PHP
Path: src/Database/functions.php
<?php
namespace mini;
use mini\Converter\ConverterRegistryInterface;
use mini\Database\DatabaseInterface;
use mini\Database\PDOService;
use mini\Database\SqlValueHydrator;
use mini\Database\SqlValue;
use PDO;
// Register services
Mini::$mini->addService(PDO::class, Lifetime::Scoped, function() {
$pdo = Mini::$mini->loadServiceConfig(PDO::class);
PDOService::configure($pdo);
return $pdo;
});
Mini::$mini->addService(DatabaseInterface::class, Lifetime::Scoped, fn() => Mini::$mini->loadServiceConfig(DatabaseInterface::class));
// Register sql-value converters for common types
Mini::$mini->phase->onEnteredState(Phase::Ready, function() {
$registry = Mini::$mini->get(ConverterRegistryInterface::class);
// =========================================================================
// PHP → SQL (for query parameters)
// Target type: 'sql-value'
// =========================================================================
// DateTime -> string (converted to sqlTimezone)
$registry->register(function(\DateTimeInterface $dt): string {
$dbTz = new \DateTimeZone(Mini::$mini->sqlTimezone);
if ($dt instanceof \DateTime) {
$dt = \DateTimeImmutable::createFromMutable($dt);
}
return $dt->setTimezone($dbTz)->format('Y-m-d H:i:s');
}, 'sql-value');
// BackedEnum -> its backing value (string or int)
$registry->register(fn(\BackedEnum $enum) => $enum->value, 'sql-value');
// UnitEnum -> string (name)
$registry->register(fn(\UnitEnum $enum) => $enum->name, 'sql-value');
// Stringable -> string
$registry->register(fn(\Stringable $obj) => (string) $obj, 'sql-value');
// SqlValue -> scalar
$registry->register(fn(SqlValue $obj) => $obj->toSqlValue(), 'sql-value');
// =========================================================================
// SQL → PHP (for entity hydration)
// Source type: 'sql-value'
// =========================================================================
// sql-value -> bool
// Handles: 0/1 (int), "0"/"1" (string), "" (empty string)
$registry->register(function(int|string $v): bool {
return $v !== 0 && $v !== '0' && $v !== '';
}, null, 'sql-value');
// sql-value -> DateTimeImmutable
// Interprets DB values in sqlTimezone, converts to application timezone.
$registry->register(function(string|int|float $v): \DateTimeImmutable {
$dbTz = new \DateTimeZone(Mini::$mini->sqlTimezone);
$appTz = new \DateTimeZone(date_default_timezone_get());
if (is_string($v)) {
// Parse in database timezone, convert to app timezone
$dt = new \DateTimeImmutable($v, $dbTz);
return $dt->setTimezone($appTz);
}
// Unix timestamps are always UTC regardless of sqlTimezone setting
if (is_float($v)) {
// Float: seconds with microsecond precision
$sec = (int) $v;
$usec = (int) (($v - $sec) * 1_000_000);
$dt = \DateTimeImmutable::createFromFormat('U u', "$sec $usec") ?: new \DateTimeImmutable("@$sec");
return $dt->setTimezone($appTz);
}
// Integer: detect seconds vs milliseconds
// Timestamps < 100 billion are seconds (covers until year ~5138)
// Timestamps >= 100 billion are milliseconds
if ($v >= 100_000_000_000) {
$sec = intdiv($v, 1000);
$usec = ($v % 1000) * 1000;
$dt = \DateTimeImmutable::createFromFormat('U u', "$sec $usec") ?: new \DateTimeImmutable("@$sec");
return $dt->setTimezone($appTz);
}
$dt = new \DateTimeImmutable("@$v");
return $dt->setTimezone($appTz);
}, null, 'sql-value');
// sql-value -> DateTime
// Interprets DB values in sqlTimezone, converts to application timezone.
$registry->register(function(string|int|float $v): \DateTime {
$dbTz = new \DateTimeZone(Mini::$mini->sqlTimezone);
$appTz = new \DateTimeZone(date_default_timezone_get());
if (is_string($v)) {
// Parse in database timezone, convert to app timezone
$dt = new \DateTime($v, $dbTz);
$dt->setTimezone($appTz);
return $dt;
}
// Unix timestamps are always UTC regardless of sqlTimezone setting
if (is_float($v)) {
$sec = (int) $v;
$usec = (int) (($v - $sec) * 1_000_000);
$dt = \DateTime::createFromFormat('U u', "$sec $usec") ?: new \DateTime("@$sec");
$dt->setTimezone($appTz);
return $dt;
}
if ($v >= 100_000_000_000) {
$sec = intdiv($v, 1000);
$usec = ($v % 1000) * 1000;
$dt = \DateTime::createFromFormat('U u', "$sec $usec") ?: new \DateTime("@$sec");
$dt->setTimezone($appTz);
return $dt;
}
$dt = new \DateTime("@$v");
$dt->setTimezone($appTz);
return $dt;
}, null, 'sql-value');
// =========================================================================
// Fallback handler for type families
// =========================================================================
$registry->fallback->listen(function(mixed $input, string $targetType, ?string $sourceType): mixed {
if ($sourceType !== 'sql-value') {
return null;
}
// sql-value -> BackedEnum (any BackedEnum subclass)
if (is_subclass_of($targetType, \BackedEnum::class)) {
return $targetType::from($input);
}
// sql-value -> SqlValueHydrator (custom value objects)
if (is_subclass_of($targetType, SqlValueHydrator::class)) {
return $targetType::fromSqlValue($input);
}
return null;
});
});
/**
* Get the database service instance
*
* Returns a lazy-loaded DatabaseInterface for executing queries.
* Works out of the box with SQLite (_database.sqlite3 in project root).
*
* Configure via environment variables:
* - DATABASE_URL: mysql://user:pass@host/dbname, postgresql://..., sqlite:///path
* - MINI_DATABASE_URL: Same format, takes precedence over DATABASE_URL
*
* @return DatabaseInterface The database service
*/
function db(): DatabaseInterface {
return Mini::$mini->get(DatabaseInterface::class);
}
/**
* Convert a value to SQL-bindable scalar
*
* Uses the 'sql-value' converter target type. Returns the value unchanged
* if it's already a scalar or null.
*
* @param mixed $value Value to convert
* @return string|int|float|bool|null SQL-bindable value
* @throws \InvalidArgumentException If value cannot be converted
*/
function sqlval(mixed $value): string|int|float|bool|null
{
if ($value === null || is_scalar($value)) {
return $value;
}
$converted = convert($value, 'sql-value');
if ($converted !== null) {
return $converted;
}
throw new \InvalidArgumentException(
'Cannot convert ' . get_debug_type($value) . ' to SQL parameter. ' .
'Implement SqlValue or register an sql-value converter.'
);
}