Browse Source
- Introduced abstract Statement class to hold common methods - Common methods currently consist of a date formatter and type caster - Moved binding tests to a trait for reuse with future driversmicrosub
J. King
7 years ago
8 changed files with 369 additions and 159 deletions
@ -0,0 +1,89 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace JKingWeb\NewsSync\Db; |
|||
|
|||
abstract class AbstractStatement implements Statement { |
|||
|
|||
abstract function runArray(array $values): Result; |
|||
abstract static function dateFormat(int $part = self::TS_BOTH): string; |
|||
|
|||
public function run(...$values): Result { |
|||
return $this->runArray($values); |
|||
} |
|||
|
|||
public function rebind(...$bindings): bool { |
|||
return $this->rebindArray($bindings); |
|||
} |
|||
|
|||
public function rebindArray(array $bindings): bool { |
|||
$this->types = []; |
|||
foreach($bindings as $binding) { |
|||
$binding = trim(strtolower($binding)); |
|||
if(!array_key_exists($binding, self::TYPES)) throw new Exception("paramTypeInvalid", $binding); |
|||
$this->types[] = self::TYPES[$binding]; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
protected function cast($v, string $t) { |
|||
switch($t) { |
|||
case "date": |
|||
return $this->formatDate($v, self::TS_DATE); |
|||
case "time": |
|||
return $this->formatDate($v, self::TS_TIME); |
|||
case "datetime": |
|||
return $this->formatDate($v, self::TS_BOTH); |
|||
case "null": |
|||
case "integer": |
|||
case "float": |
|||
case "binary": |
|||
case "string": |
|||
case "boolean": |
|||
if($t=="binary") $t = "string"; |
|||
$value = $v; |
|||
try{ |
|||
settype($value, $t); |
|||
} catch(\Throwable $e) { |
|||
// handle objects |
|||
$value = $v; |
|||
if($value instanceof \DateTimeInterface) { |
|||
$value = $value->getTimestamp(); |
|||
if($t=="string") $value = $this->formatDate($value, self::TS_BOTH); |
|||
settype($value, $t); |
|||
} else { |
|||
$value = null; |
|||
settype($value, $t); |
|||
} |
|||
} |
|||
return $value; |
|||
default: |
|||
throw new Exception("paramTypeUnknown", $type); |
|||
} |
|||
} |
|||
|
|||
protected function formatDate($date, int $part = self::TS_BOTH) { |
|||
// Force UTC. |
|||
$timezone = date_default_timezone_get(); |
|||
date_default_timezone_set('UTC'); |
|||
// convert input to a Unix timestamp |
|||
// FIXME: there are more kinds of date representations |
|||
if($date instanceof \DateTimeInterface) { |
|||
$time = $date->getTimestamp(); |
|||
} else if(is_numeric($date)) { |
|||
$time = (int) $date; |
|||
} else if($date===null) { |
|||
return null; |
|||
} else if(is_string($date)) { |
|||
$time = strtotime($date); |
|||
if($time===false) return null; |
|||
} else if (is_bool($date)) { |
|||
return null; |
|||
} else { |
|||
$time = (int) $date; |
|||
} |
|||
// ISO 8601 with space in the middle instead of T. |
|||
$date = date($this->dateFormat($part), $time); |
|||
date_default_timezone_set($timezone); |
|||
return $date; |
|||
} |
|||
} |
@ -0,0 +1,240 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace JKingWeb\NewsSync\Test\Db; |
|||
use JKingWeb\NewsSync\Db\Statement; |
|||
use JKingWeb\NewsSync\Db\Driver; |
|||
|
|||
trait BindingTests { |
|||
|
|||
function testBindMissingValue() { |
|||
$s = new self::$imp($this->c, $this->s); |
|||
$val = $s->runArray()->get()['value']; |
|||
$this->assertSame(null, $val); |
|||
} |
|||
|
|||
function testBindNull() { |
|||
$input = null; |
|||
$exp = [ |
|||
"null" => null, |
|||
"integer" => null, |
|||
"float" => null, |
|||
"date" => null, |
|||
"time" => null, |
|||
"datetime" => null, |
|||
"binary" => null, |
|||
"string" => null, |
|||
"boolean" => null, |
|||
]; |
|||
$this->checkBinding($input, $exp); |
|||
} |
|||
|
|||
function testBindTrue() { |
|||
$input = true; |
|||
$exp = [ |
|||
"null" => null, |
|||
"integer" => 1, |
|||
"float" => 1.0, |
|||
"date" => null, |
|||
"time" => null, |
|||
"datetime" => null, |
|||
"binary" => "1", |
|||
"string" => "1", |
|||
"boolean" => 1, |
|||
]; |
|||
$this->checkBinding($input, $exp); |
|||
} |
|||
|
|||
function testBindFalse() { |
|||
$input = false; |
|||
$exp = [ |
|||
"null" => null, |
|||
"integer" => 0, |
|||
"float" => 0.0, |
|||
"date" => null, |
|||
"time" => null, |
|||
"datetime" => null, |
|||
"binary" => "", |
|||
"string" => "", |
|||
"boolean" => 0, |
|||
]; |
|||
$this->checkBinding($input, $exp); |
|||
} |
|||
|
|||
function testBindInteger() { |
|||
$input = 2112; |
|||
$exp = [ |
|||
"null" => null, |
|||
"integer" => 2112, |
|||
"float" => 2112.0, |
|||
"date" => date(self::$imp::dateFormat(Statement::TS_DATE), 2112), |
|||
"time" => date(self::$imp::dateFormat(Statement::TS_TIME), 2112), |
|||
"datetime" => date(self::$imp::dateFormat(Statement::TS_BOTH), 2112), |
|||
"binary" => "2112", |
|||
"string" => "2112", |
|||
"boolean" => 1, |
|||
]; |
|||
$this->checkBinding($input, $exp); |
|||
} |
|||
|
|||
function testBindIntegerZero() { |
|||
$input = 0; |
|||
$exp = [ |
|||
"null" => null, |
|||
"integer" => 0, |
|||
"float" => 0.0, |
|||
"date" => date(self::$imp::dateFormat(Statement::TS_DATE), 0), |
|||
"time" => date(self::$imp::dateFormat(Statement::TS_TIME), 0), |
|||
"datetime" => date(self::$imp::dateFormat(Statement::TS_BOTH), 0), |
|||
"binary" => "0", |
|||
"string" => "0", |
|||
"boolean" => 0, |
|||
]; |
|||
$this->checkBinding($input, $exp); |
|||
} |
|||
|
|||
function testBindFloat() { |
|||
$input = 2112.0; |
|||
$exp = [ |
|||
"null" => null, |
|||
"integer" => 2112, |
|||
"float" => 2112.0, |
|||
"date" => date(self::$imp::dateFormat(Statement::TS_DATE), 2112), |
|||
"time" => date(self::$imp::dateFormat(Statement::TS_TIME), 2112), |
|||
"datetime" => date(self::$imp::dateFormat(Statement::TS_BOTH), 2112), |
|||
"binary" => "2112", |
|||
"string" => "2112", |
|||
"boolean" => 1, |
|||
]; |
|||
$this->checkBinding($input, $exp); |
|||
} |
|||
|
|||
function testBindFloatZero() { |
|||
$input = 0.0; |
|||
$exp = [ |
|||
"null" => null, |
|||
"integer" => 0, |
|||
"float" => 0.0, |
|||
"date" => date(self::$imp::dateFormat(Statement::TS_DATE), 0), |
|||
"time" => date(self::$imp::dateFormat(Statement::TS_TIME), 0), |
|||
"datetime" => date(self::$imp::dateFormat(Statement::TS_BOTH), 0), |
|||
"binary" => "0", |
|||
"string" => "0", |
|||
"boolean" => 0, |
|||
]; |
|||
$this->checkBinding($input, $exp); |
|||
} |
|||
|
|||
function testBindAsciiString() { |
|||
$input = "Random string"; |
|||
$exp = [ |
|||
"null" => null, |
|||
"integer" => 0, |
|||
"float" => 0.0, |
|||
"date" => null, |
|||
"time" => null, |
|||
"datetime" => null, |
|||
"binary" => $input, |
|||
"string" => $input, |
|||
"boolean" => 1, |
|||
]; |
|||
$this->checkBinding($input, $exp); |
|||
} |
|||
|
|||
function testBindUtf8String() { |
|||
$input = "é"; |
|||
$exp = [ |
|||
"null" => null, |
|||
"integer" => 0, |
|||
"float" => 0.0, |
|||
"date" => null, |
|||
"time" => null, |
|||
"datetime" => null, |
|||
"binary" => $input, |
|||
"string" => $input, |
|||
"boolean" => 1, |
|||
]; |
|||
$this->checkBinding($input, $exp); |
|||
} |
|||
|
|||
function testBindBinaryString() { |
|||
// FIXME: This test may be unreliable; SQLite happily stores invalid UTF-8 text as bytes untouched, but other engines probably don't do this |
|||
$input = chr(233); |
|||
$exp = [ |
|||
"null" => null, |
|||
"integer" => 0, |
|||
"float" => 0.0, |
|||
"date" => null, |
|||
"time" => null, |
|||
"datetime" => null, |
|||
"binary" => $input, |
|||
"string" => $input, |
|||
"boolean" => 1, |
|||
]; |
|||
$this->checkBinding($input, $exp); |
|||
} |
|||
|
|||
function testBindIso8601DateString() { |
|||
$input = "2017-01-09T13:11:17"; |
|||
$time = strtotime($input); |
|||
$exp = [ |
|||
"null" => null, |
|||
"integer" => 2017, |
|||
"float" => 2017.0, |
|||
"date" => date(self::$imp::dateFormat(Statement::TS_DATE), $time), |
|||
"time" => date(self::$imp::dateFormat(Statement::TS_TIME), $time), |
|||
"datetime" => date(self::$imp::dateFormat(Statement::TS_BOTH), $time), |
|||
"binary" => $input, |
|||
"string" => $input, |
|||
"boolean" => 1, |
|||
]; |
|||
$this->checkBinding($input, $exp); |
|||
} |
|||
|
|||
function testBindArbitraryDateString() { |
|||
$input = "Today"; |
|||
$time = strtotime($input); |
|||
$exp = [ |
|||
"null" => null, |
|||
"integer" => 0, |
|||
"float" => 0.0, |
|||
"date" => date(self::$imp::dateFormat(Statement::TS_DATE), $time), |
|||
"time" => date(self::$imp::dateFormat(Statement::TS_TIME), $time), |
|||
"datetime" => date(self::$imp::dateFormat(Statement::TS_BOTH), $time), |
|||
"binary" => $input, |
|||
"string" => $input, |
|||
"boolean" => 1, |
|||
]; |
|||
$this->checkBinding($input, $exp); |
|||
} |
|||
|
|||
function testBindMutableDateObject($class = '\DateTime') { |
|||
$input = new $class("Noon Today"); |
|||
$time = $input->getTimestamp(); |
|||
$exp = [ |
|||
"null" => null, |
|||
"integer" => $time, |
|||
"float" => (float) $time, |
|||
"date" => date(self::$imp::dateFormat(Statement::TS_DATE), $time), |
|||
"time" => date(self::$imp::dateFormat(Statement::TS_TIME), $time), |
|||
"datetime" => date(self::$imp::dateFormat(Statement::TS_BOTH), $time), |
|||
"binary" => date(self::$imp::dateFormat(Statement::TS_BOTH), $time), |
|||
"string" => date(self::$imp::dateFormat(Statement::TS_BOTH), $time), |
|||
"boolean" => 1, |
|||
]; |
|||
$this->checkBinding($input, $exp); |
|||
} |
|||
|
|||
function testBindImmutableDateObject() { |
|||
$this->testBindMutableDateObject('\DateTimeImmutable'); |
|||
} |
|||
|
|||
protected function checkBinding($input, array $expectations) { |
|||
$s = new self::$imp($this->c, $this->s); |
|||
$types = array_unique(Statement::TYPES); |
|||
foreach($types as $type) { |
|||
$s->rebindArray([$type]); |
|||
$val = $s->runArray([$input])->get()['value']; |
|||
$this->assertSame($expectations[$type], $val, "Type $type failed comparison."); |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue