diff --git a/lib/Db/AbstractStatement.php b/lib/Db/AbstractStatement.php index f787cbcf..57185ee8 100644 --- a/lib/Db/AbstractStatement.php +++ b/lib/Db/AbstractStatement.php @@ -7,12 +7,14 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Db; use JKingWeb\Arsse\Misc\Date; +use JKingWeb\Arsse\Misc\ValueInfo; abstract class AbstractStatement implements Statement { protected $types = []; protected $isNullable = []; abstract public function runArray(array $values = []): Result; + abstract protected function bindValue($value, string $type, int $position): bool; public function run(...$values): Result { return $this->runArray($values); @@ -51,31 +53,41 @@ abstract class AbstractStatement implements Statement { protected function cast($v, string $t, bool $nullable) { switch ($t) { case "datetime": + $v = Date::transform($v, "sql"); if (is_null($v) && !$nullable) { $v = 0; - } - return Date::transform($v, "sql"); - case "integer": - case "float": - case "binary": - case "string": - case "boolean": - if ($t=="binary") { - $t = "string"; - } - if ($v instanceof \DateTimeInterface) { - if ($t=="string") { - return Date::transform($v, "sql"); - } else { - $v = $v->getTimestamp(); - settype($v, $t); - } - } else { - settype($v, $t); + $v = Date::transform($v, "sql"); } return $v; + case "integer": + return ValueInfo::normalize($v, ValueInfo::T_INT | ($nullable ? ValueInfo::M_NULL : 0), null, "sql"); + case "float": + return ValueInfo::normalize($v, ValueInfo::T_FLOAT | ($nullable ? ValueInfo::M_NULL : 0), null, "sql"); + case "binary": + case "string": + return ValueInfo::normalize($v, ValueInfo::T_STRING | ($nullable ? ValueInfo::M_NULL : 0), null, "sql"); + case "boolean": + $v = ValueInfo::normalize($v, ValueInfo::T_BOOL | ($nullable ? ValueInfo::M_NULL : 0), null, "sql"); + return is_null($v) ? $v : (int) $v; default: throw new Exception("paramTypeUnknown", $type); // @codeCoverageIgnore } } + + protected function bindValues(array $values, int $offset = 0): int { + $a = $offset; + foreach ($values as $value) { + if (is_array($value)) { + // recursively flatten any arrays, which may be provided for SET or IN() clauses + $a += $this->bindValues($value, $a); + } elseif (array_key_exists($a, $this->types)) { + $value = $this->cast($value, $this->types[$a], $this->isNullable[$a]); + $this->bindValue($value, $this->types[$a], $a+1); + $a++; + } else { + throw new Exception("paramTypeMissing", $a+1); + } + } + return $a - $offset; + } } diff --git a/lib/Db/PDOStatement.php b/lib/Db/PDOStatement.php index d05255ca..cfd9cefc 100644 --- a/lib/Db/PDOStatement.php +++ b/lib/Db/PDOStatement.php @@ -49,34 +49,7 @@ class PDOStatement extends AbstractStatement { return new PDOResult($this->st, [$changes, $lastId]); } - protected function bindValues(array $values, int $offset = 0): int { - $a = $offset; - foreach ($values as $value) { - if (is_array($value)) { - // recursively flatten any arrays, which may be provided for SET or IN() clauses - $a += $this->bindValues($value, $a); - } elseif (array_key_exists($a, $this->types)) { - // if the parameter type is something other than the known values, this is an error - assert(array_key_exists($this->types[$a], self::BINDINGS), new Exception("paramTypeUnknown", $this->types[$a])); - // if the parameter type is null or the value is null (and the type is nullable), just bind null - if ($this->types[$a]=="null" || ($this->isNullable[$a] && is_null($value))) { - $this->st->bindValue($a+1, null, \PDO::PARAM_NULL); - } else { - // otherwise cast the value to the right type and bind the result - $type = self::BINDINGS[$this->types[$a]]; - $value = $this->cast($value, $this->types[$a], $this->isNullable[$a]); - // re-adjust for null casts - if ($value===null) { - $type = \PDO::PARAM_NULL; - } - // perform binding - $this->st->bindValue($a+1, $value, $type); - } - $a++; - } else { - throw new Exception("paramTypeMissing", $a+1); - } - } - return $a - $offset; + protected function bindValue($value, string $type, int $position): bool { + return $this->st->bindValue($position, $value, is_null($value) ? \PDO::PARAM_NULL : self::BINDINGS[$type]); } } diff --git a/lib/Db/SQLite3/Statement.php b/lib/Db/SQLite3/Statement.php index ee3cd446..ab07b47b 100644 --- a/lib/Db/SQLite3/Statement.php +++ b/lib/Db/SQLite3/Statement.php @@ -56,34 +56,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement { return new Result($r, [$changes, $lastId], $this); } - protected function bindValues(array $values, int $offset = 0): int { - $a = $offset; - foreach ($values as $value) { - if (is_array($value)) { - // recursively flatten any arrays, which may be provided for SET or IN() clauses - $a += $this->bindValues($value, $a); - } elseif (array_key_exists($a, $this->types)) { - // if the parameter type is something other than the known values, this is an error - assert(array_key_exists($this->types[$a], self::BINDINGS), new Exception("paramTypeUnknown", $this->types[$a])); - // if the parameter type is null or the value is null (and the type is nullable), just bind null - if ($this->types[$a]=="null" || ($this->isNullable[$a] && is_null($value))) { - $this->st->bindValue($a+1, null, \SQLITE3_NULL); - } else { - // otherwise cast the value to the right type and bind the result - $type = self::BINDINGS[$this->types[$a]]; - $value = $this->cast($value, $this->types[$a], $this->isNullable[$a]); - // re-adjust for null casts - if ($value===null) { - $type = \SQLITE3_NULL; - } - // perform binding - $this->st->bindValue($a+1, $value, $type); - } - $a++; - } else { - throw new Exception("paramTypeMissing", $a+1); - } - } - return $a - $offset; + protected function bindValue($value, string $type, int $position): bool { + return $this->st->bindValue($position, $value, is_null($value) ? \SQLITE3_NULL : self::BINDINGS[$type]); } } diff --git a/lib/Misc/ValueInfo.php b/lib/Misc/ValueInfo.php index a025bb56..870dad0f 100644 --- a/lib/Misc/ValueInfo.php +++ b/lib/Misc/ValueInfo.php @@ -34,7 +34,7 @@ class ValueInfo { const M_STRICT = 1 << 30; // throw an exception if the type doesn't match const M_ARRAY = 1 << 31; // the value should be a flat array of values of the specified type; indexed and associative are both acceptable - public static function normalize($value, int $type, string $dateFormat = null) { + public static function normalize($value, int $type, string $dateInFormat = null, $dateOutFormat = null) { $allowNull = ($type & self::M_NULL); $strict = ($type & (self::M_STRICT | self::M_DROP)); $drop = ($type & self::M_DROP); @@ -48,7 +48,7 @@ class ValueInfo { if ($arrayVal) { $value = self::normalize($value, self::T_ARRAY); foreach ($value as $key => $v) { - $value[$key] = self::normalize($v, $type | ($allowNull ? self::M_NULL : 0) | ($strict ? self::M_STRICT : 0) | ($drop ? self::M_DROP : 0), $dateFormat); + $value[$key] = self::normalize($v, $type | ($allowNull ? self::M_NULL : 0) | ($strict ? self::M_STRICT : 0) | ($drop ? self::M_DROP : 0), $dateInFormat, $dateOutFormat); } return $value; } @@ -131,12 +131,14 @@ class ValueInfo { if (is_string($value)) { return $value; } + $dateOutFormat = $dateOutFormat ?? "iso8601"; + $dateOutFormat = isset(Date::FORMAT[$dateOutFormat]) ? Date::FORMAT[$dateOutFormat][1] : $dateOutFormat; if ($value instanceof \DateTimeImmutable) { - return $value->setTimezone(new \DateTimeZone("UTC"))->format(Date::FORMAT['iso8601'][1]); + return $value->setTimezone(new \DateTimeZone("UTC"))->format($dateOutFormat); } elseif ($value instanceof \DateTime) { $out = clone $value; $out->setTimezone(new \DateTimeZone("UTC")); - return $out->format(Date::FORMAT['iso8601'][1]); + return $out->format($dateOutFormat); } elseif (is_float($value) && is_finite($value)) { $out = (string) $value; if (!strpos($out, "E")) { @@ -175,9 +177,9 @@ class ValueInfo { return \DateTime::createFromFormat("U.u", sprintf("%F", $value), new \DateTimeZone("UTC")); } elseif (is_string($value)) { try { - if (!is_null($dateFormat)) { + if (!is_null($dateInFormat)) { $out = false; - if ($dateFormat=="microtime") { + if ($dateInFormat=="microtime") { // PHP is not able to correctly handle the output of microtime() as the input of DateTime::createFromFormat(), so we fudge it to look like a float if (preg_match("<^0\.\d{6}00 \d+$>", $value)) { $value = substr($value, 11).".".substr($value, 2, 6); @@ -185,10 +187,10 @@ class ValueInfo { throw new \Exception; } } - $f = isset(Date::FORMAT[$dateFormat]) ? Date::FORMAT[$dateFormat][0] : $dateFormat; - if ($dateFormat=="iso8601" || $dateFormat=="iso8601m") { + $f = isset(Date::FORMAT[$dateInFormat]) ? Date::FORMAT[$dateInFormat][0] : $dateInFormat; + if ($dateInFormat=="iso8601" || $dateInFormat=="iso8601m") { // DateTime::createFromFormat() doesn't provide one catch-all for ISO 8601 timezone specifiers, so we try all of them till one works - if ($dateFormat=="iso8601m") { + if ($dateInFormat=="iso8601m") { $f2 = Date::FORMAT["iso8601"][0]; $zones = [$f."", $f."\Z", $f."P", $f."O", $f2."", $f2."\Z", $f2."P", $f2."O"]; } else { diff --git a/tests/cases/Db/SQLite3/TestStatement.php b/tests/cases/Db/SQLite3/TestStatement.php index dcb0e2ac..5a195a84 100644 --- a/tests/cases/Db/SQLite3/TestStatement.php +++ b/tests/cases/Db/SQLite3/TestStatement.php @@ -90,7 +90,7 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { [true, "strict float", "1.0"], [true, "strict string", "'1'"], [true, "strict binary", "x'31'"], - [true, "strict datetime", "'1970-01-01 00:00:01'"], + [true, "strict datetime", "'1970-01-01 00:00:00'"], [true, "strict boolean", "1"], // false [false, "integer", "0"], @@ -197,14 +197,14 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { [chr(233).chr(233), "strict datetime", "'1970-01-01 00:00:00'"], [chr(233).chr(233), "strict boolean", "1"], // ISO 8601 date string - ["2017-01-09T13:11:17", "integer", "2017"], - ["2017-01-09T13:11:17", "float", "2017.0"], + ["2017-01-09T13:11:17", "integer", "0"], + ["2017-01-09T13:11:17", "float", "0.0"], ["2017-01-09T13:11:17", "string", "'2017-01-09T13:11:17'"], ["2017-01-09T13:11:17", "binary", "x'323031372d30312d30395431333a31313a3137'"], ["2017-01-09T13:11:17", "datetime", "'2017-01-09 13:11:17'"], ["2017-01-09T13:11:17", "boolean", "1"], - ["2017-01-09T13:11:17", "strict integer", "2017"], - ["2017-01-09T13:11:17", "strict float", "2017.0"], + ["2017-01-09T13:11:17", "strict integer", "0"], + ["2017-01-09T13:11:17", "strict float", "0.0"], ["2017-01-09T13:11:17", "strict string", "'2017-01-09T13:11:17'"], ["2017-01-09T13:11:17", "strict binary", "x'323031372d30312d30395431333a31313a3137'"], ["2017-01-09T13:11:17", "strict datetime", "'2017-01-09 13:11:17'"], diff --git a/tests/cases/Db/SQLite3PDO/TestStatement.php b/tests/cases/Db/SQLite3PDO/TestStatement.php index 9a269a1e..8fe70861 100644 --- a/tests/cases/Db/SQLite3PDO/TestStatement.php +++ b/tests/cases/Db/SQLite3PDO/TestStatement.php @@ -91,7 +91,7 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { [true, "strict float", "'1'"], [true, "strict string", "'1'"], [true, "strict binary", "x'31'"], - [true, "strict datetime", "'1970-01-01 00:00:01'"], + [true, "strict datetime", "'1970-01-01 00:00:00'"], [true, "strict boolean", "1"], // false [false, "integer", "0"], @@ -198,14 +198,14 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { [chr(233).chr(233), "strict datetime", "'1970-01-01 00:00:00'"], [chr(233).chr(233), "strict boolean", "1"], // ISO 8601 date string - ["2017-01-09T13:11:17", "integer", "2017"], - ["2017-01-09T13:11:17", "float", "'2017'"], + ["2017-01-09T13:11:17", "integer", "0"], + ["2017-01-09T13:11:17", "float", "'0'"], ["2017-01-09T13:11:17", "string", "'2017-01-09T13:11:17'"], ["2017-01-09T13:11:17", "binary", "x'323031372d30312d30395431333a31313a3137'"], ["2017-01-09T13:11:17", "datetime", "'2017-01-09 13:11:17'"], ["2017-01-09T13:11:17", "boolean", "1"], - ["2017-01-09T13:11:17", "strict integer", "2017"], - ["2017-01-09T13:11:17", "strict float", "'2017'"], + ["2017-01-09T13:11:17", "strict integer", "0"], + ["2017-01-09T13:11:17", "strict float", "'0'"], ["2017-01-09T13:11:17", "strict string", "'2017-01-09T13:11:17'"], ["2017-01-09T13:11:17", "strict binary", "x'323031372d30312d30395431333a31313a3137'"], ["2017-01-09T13:11:17", "strict datetime", "'2017-01-09 13:11:17'"],