From 8e53675753cae934cb7adb549f5b5b9bb8a250c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 14 Oct 2023 17:05:06 +0200 Subject: [PATCH] Make Connection constructor protected (#1138) --- docs/.readthedocs.yaml | 16 ++++ phpstan.neon.dist | 2 +- src/Field.php | 4 + src/Persistence.php | 6 +- src/Persistence/Sql/Connection.php | 4 +- src/Persistence/Sql/Expression.php | 10 ++- src/Persistence/Sql/Oracle/PlatformTrait.php | 5 +- .../Sql/Postgresql/PlatformTrait.php | 10 +-- tests/FieldTest.php | 80 ++++++++++++++++--- tests/Persistence/Sql/ConnectionTest.php | 51 +++--------- tests/Persistence/Sql/ExpressionTest.php | 5 +- tests/Persistence/Sql/QueryTest.php | 14 ++-- tests/Schema/TestCaseTest.php | 6 +- tests/TypecastingTest.php | 46 +++++++++++ 14 files changed, 183 insertions(+), 76 deletions(-) create mode 100644 docs/.readthedocs.yaml diff --git a/docs/.readthedocs.yaml b/docs/.readthedocs.yaml new file mode 100644 index 000000000..b631017e1 --- /dev/null +++ b/docs/.readthedocs.yaml @@ -0,0 +1,16 @@ +version: 2 + +build: + # https://github.com/readthedocs/readthedocs.org/issues/8861 + os: ubuntu-22.04 + tools: + # https://github.com/readthedocs/readthedocs.org/issues/9719 + python: '3' + +python: + install: + # https://github.com/readthedocs/readthedocs.org/issues/10806 + - requirements: docs/requirements.txt + +formats: + - pdf diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 14f205da7..d1cacbc67 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -40,7 +40,7 @@ parameters: - message: '~^Class Doctrine\\DBAL\\Platforms\\SqlitePlatform referenced with incorrect case: Doctrine\\DBAL\\Platforms\\SQLitePlatform\.$~' path: '*' - count: 31 + count: 28 # TODO these rules are generated, this ignores should be fixed in the code # for src/Schema/TestCase.php diff --git a/src/Field.php b/src/Field.php index 238b5cae4..b2d7b30c6 100644 --- a/src/Field.php +++ b/src/Field.php @@ -153,6 +153,7 @@ public function normalize($value) break; case 'float': + case 'decimal': case 'atk4_money': $value = preg_replace('~\s+|[`\']|,(?=.*\.)~', '', $value); @@ -163,6 +164,7 @@ public function normalize($value) case 'boolean': case 'integer': case 'float': + case 'decimal': case 'atk4_money': if ($value === '') { $value = null; @@ -178,6 +180,7 @@ public function normalize($value) case 'text': case 'integer': case 'float': + case 'decimal': case 'atk4_money': if (is_bool($value)) { throw new Exception('Must not be boolean type'); @@ -223,6 +226,7 @@ public function normalize($value) break; case 'integer': case 'float': + case 'decimal': case 'atk4_money': if ($this->required && !$value) { throw new Exception('Must not be a zero'); diff --git a/src/Persistence.php b/src/Persistence.php index 207f1f460..352ee418b 100644 --- a/src/Persistence.php +++ b/src/Persistence.php @@ -359,7 +359,7 @@ public function typecastSaveField(Field $field, $value) try { $v = $this->_typecastSaveField($field, $value); if ($v !== null && !is_scalar($v)) { // @phpstan-ignore-line - throw new Exception('Unexpected non-scalar value'); + throw new \TypeError('Unexpected non-scalar value'); } return $v; @@ -382,7 +382,7 @@ public function typecastLoadField(Field $field, $value) if ($value === null) { return null; } elseif (!is_scalar($value)) { // @phpstan-ignore-line - throw new Exception('Unexpected non-scalar value'); + throw new \TypeError('Unexpected non-scalar value'); } try { @@ -446,7 +446,7 @@ protected function _typecastSaveField(Field $field, $value) protected function _typecastLoadField(Field $field, $value) { // TODO casting optionally to null should be handled by type itself solely - if ($value === '' && in_array($field->type, ['boolean', 'integer', 'float', 'datetime', 'date', 'time', 'json', 'object'], true)) { + if ($value === '' && in_array($field->type, ['boolean', 'integer', 'float', 'decimal', 'datetime', 'date', 'time', 'json', 'object'], true)) { return null; } diff --git a/src/Persistence/Sql/Connection.php b/src/Persistence/Sql/Connection.php index 347b28ca6..bdf360832 100644 --- a/src/Persistence/Sql/Connection.php +++ b/src/Persistence/Sql/Connection.php @@ -45,7 +45,7 @@ abstract class Connection /** * @param array $defaults */ - public function __construct(array $defaults = []) + protected function __construct(array $defaults = []) { $this->setDefaults($defaults); } @@ -177,7 +177,7 @@ public static function resolveConnectionClass(string $driverName): string } /** - * Connect to database and return connection class. + * Connect to database and return connection instance. * * @param string|array|DbalConnection|DbalDriverConnection $dsn * @param string|null $user diff --git a/src/Persistence/Sql/Expression.php b/src/Persistence/Sql/Expression.php index 29e615ab0..fda6f82e3 100644 --- a/src/Persistence/Sql/Expression.php +++ b/src/Persistence/Sql/Expression.php @@ -445,7 +445,15 @@ public function getDebugQuery(): string { [$sql, $params] = $this->render(); - if (class_exists('SqlFormatter')) { // requires optional "jdorn/sql-formatter" package + if (class_exists(\SqlFormatter::class)) { // requires optional "jdorn/sql-formatter" package + \Closure::bind(static function () { + // fix latest/1.2.16 release from 2013-11-28 + if (end(\SqlFormatter::$reserved_toplevel) === 'INTERSECT') { + \SqlFormatter::$reserved_toplevel[] = 'OFFSET'; + \SqlFormatter::$reserved_toplevel[] = 'FETCH'; + } + }, null, \SqlFormatter::class)(); + $sql = preg_replace('~ +(?=\n|$)|(?<=:) (?=\w)~', '', \SqlFormatter::format($sql, false)); } diff --git a/src/Persistence/Sql/Oracle/PlatformTrait.php b/src/Persistence/Sql/Oracle/PlatformTrait.php index 02f0340f4..df9c2986c 100644 --- a/src/Persistence/Sql/Oracle/PlatformTrait.php +++ b/src/Persistence/Sql/Oracle/PlatformTrait.php @@ -71,9 +71,8 @@ public function getCreateAutoincrementSql($name, $table, $start = 1) $aiSequenceName = $this->getIdentitySequenceName($tableIdentifier->getQuotedName($this), $nameIdentifier->getQuotedName($this)); assert(str_starts_with($sqls[count($sqls) - 1], 'CREATE TRIGGER ' . $aiTriggerName . "\n")); - $conn = new Connection(); $pkSeq = \Closure::bind(fn () => $this->normalizeIdentifier($aiSequenceName), $this, OraclePlatform::class)()->getName(); - $sqls[count($sqls) - 1] = $conn->expr( + $sqls[count($sqls) - 1] = (new Expression( // else branch should be maybe (because of concurrency) put into after update trigger str_replace('[pk_seq]', '\'' . str_replace('\'', '\'\'', $pkSeq) . '\'', <<<'EOF' CREATE TRIGGER {{trigger}} @@ -100,7 +99,7 @@ public function getCreateAutoincrementSql($name, $table, $start = 1) 'pk' => $nameIdentifier->getName(), 'pk_seq' => $pkSeq, ] - )->render()[0]; + ))->render()[0]; return $sqls; } diff --git a/src/Persistence/Sql/Postgresql/PlatformTrait.php b/src/Persistence/Sql/Postgresql/PlatformTrait.php index d943cd736..8595bc708 100644 --- a/src/Persistence/Sql/Postgresql/PlatformTrait.php +++ b/src/Persistence/Sql/Postgresql/PlatformTrait.php @@ -82,9 +82,7 @@ protected function getCreateAutoincrementSql(Table $table, Column $pkColumn): ar $pkSeqName = $this->getIdentitySequenceName($table->getName(), $pkColumn->getName()); - $conn = new Connection(); - - $sqls[] = $conn->expr( + $sqls[] = (new Expression( // else branch should be maybe (because of concurrency) put into after update trigger // with pure nextval instead of setval with a loop like in Oracle trigger str_replace('[pk_seq]', '\'' . $pkSeqName . '\'', <<<'EOF' @@ -111,9 +109,9 @@ protected function getCreateAutoincrementSql(Table $table, Column $pkColumn): ar 'pk_seq' => $pkSeqName, 'trigger_func' => $table->getName() . '_AI_FUNC', ] - )->render()[0]; + ))->render()[0]; - $sqls[] = $conn->expr( + $sqls[] = (new Expression( <<<'EOF' CREATE TRIGGER {trigger} BEFORE INSERT OR UPDATE @@ -126,7 +124,7 @@ protected function getCreateAutoincrementSql(Table $table, Column $pkColumn): ar 'trigger' => $table->getShortestName($table->getNamespaceName()) . '_AI_PK', 'trigger_func' => $table->getName() . '_AI_FUNC', ] - )->render()[0]; + ))->render()[0]; return $sqls; } diff --git a/tests/FieldTest.php b/tests/FieldTest.php index 97d388e5f..9bb1a284b 100644 --- a/tests/FieldTest.php +++ b/tests/FieldTest.php @@ -23,7 +23,7 @@ public function testDefaultValue(): void self::assertSame('abc', $m->get('withdefault')); } - public function testDirty1(): void + public function testDirty(): void { $m = new Model(); $m->addField('foo', ['default' => 'abc']); @@ -67,7 +67,7 @@ public function testCompare(): void self::assertTrue($m->compare('foo', 'zzz')); } - public function testNotNullable1(): void + public function testNotNullableNullException(): void { $m = new Model(); $m->addField('foo', ['nullable' => false]); @@ -79,7 +79,17 @@ public function testNotNullable1(): void $m->set('foo', null); } - public function testRequired1(): void + public function testRequiredNullException(): void + { + $m = new Model(); + $m->addField('foo', ['required' => true]); + $m = $m->createEntity(); + + $this->expectException(ValidationException::class); + $m->set('foo', null); + } + + public function testRequiredStringEmptyException(): void { $m = new Model(); $m->addField('foo', ['required' => true]); @@ -89,17 +99,54 @@ public function testRequired1(): void $m->set('foo', ''); } - public function testRequired11(): void + public function testRequiredBinaryEmptyException(): void + { + $m = new Model(); + $m->addField('foo', ['type' => 'binary', 'required' => true]); + $m = $m->createEntity(); + + $m->set('foo', '0'); + + $this->expectException(ValidationException::class); + $m->set('foo', ''); + } + + public function testRequiredStringZeroException(): void { $m = new Model(); $m->addField('foo', ['required' => true]); $m = $m->createEntity(); $this->expectException(ValidationException::class); - $m->set('foo', null); + $m->set('foo', '0'); + } + + /** + * @dataProvider provideRequiredNumericZeroExceptionCases + */ + public function testRequiredNumericZeroException(string $type): void + { + $m = new Model(); + $m->addField('foo', ['type' => $type, 'required' => true]); + $m = $m->createEntity(); + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must not be a zero'); + $m->set('foo', 0); + } + + /** + * @return iterable> + */ + public function provideRequiredNumericZeroExceptionCases(): iterable + { + yield ['integer']; + yield ['float']; + yield ['decimal']; + yield ['atk4_money']; } - public function testNotNullable2(): void + public function testNotNullableNullInsertException(): void { $this->setDb([ 'user' => [ @@ -115,7 +162,7 @@ public function testNotNullable2(): void $m->insert(['surname' => 'qq']); } - public function testRequired2(): void + public function testRequiredStringEmptyInsertException(): void { $this->setDb([ 'user' => [ @@ -131,7 +178,7 @@ public function testRequired2(): void $m->insert(['surname' => 'qq', 'name' => '']); } - public function testNotNullable3(): void + public function testNotNullableNullLoadException(): void { $this->setDb([ 'user' => [ @@ -148,7 +195,7 @@ public function testNotNullable3(): void $m->save(['name' => null]); } - public function testNotNullable4(): void + public function testNotNullableInsertWithDefault(): void { $this->setDb([ 'user' => [ @@ -277,6 +324,7 @@ public function testEnum3(): void $m = $m->createEntity(); $this->expectException(Exception::class); + $this->expectExceptionMessage('Must not be boolean type'); $m->set('foo', true); } @@ -580,6 +628,7 @@ public function testNormalizeException1(): void $m = $m->createEntity(); $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must be scalar'); $m->set('foo', []); } @@ -590,6 +639,7 @@ public function testNormalizeException2(): void $m = $m->createEntity(); $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must be scalar'); $m->set('foo', []); } @@ -600,6 +650,7 @@ public function testNormalizeException3(): void $m = $m->createEntity(); $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must be scalar'); $m->set('foo', []); } @@ -610,6 +661,7 @@ public function testNormalizeException4(): void $m = $m->createEntity(); $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must be scalar'); $m->set('foo', []); } @@ -620,6 +672,7 @@ public function testNormalizeException5(): void $m = $m->createEntity(); $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must be scalar'); $m->set('foo', []); } @@ -630,6 +683,7 @@ public function testNormalizeException6(): void $m = $m->createEntity(); $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must be instance of DateTimeInterface'); $m->set('foo', []); } @@ -640,6 +694,7 @@ public function testNormalizeException7(): void $m = $m->createEntity(); $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must be instance of DateTimeInterface'); $m->set('foo', []); } @@ -650,6 +705,7 @@ public function testNormalizeException8(): void $m = $m->createEntity(); $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must be instance of DateTimeInterface'); $m->set('foo', []); } @@ -660,6 +716,7 @@ public function testNormalizeException9(): void $m = $m->createEntity(); $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must be numeric'); $m->set('foo', '123---456'); } @@ -670,6 +727,7 @@ public function testNormalizeException10(): void $m = $m->createEntity(); $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must be numeric'); $m->set('foo', '123---456'); } @@ -680,6 +738,7 @@ public function testNormalizeException11(): void $m = $m->createEntity(); $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must be numeric'); $m->set('foo', '123---456'); } @@ -690,6 +749,7 @@ public function testNormalizeException12(): void $m = $m->createEntity(); $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must be an array'); $m->set('foo', 'ABC'); } @@ -700,6 +760,7 @@ public function testNormalizeException13(): void $m = $m->createEntity(); $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must be an object'); $m->set('foo', 'ABC'); } @@ -710,6 +771,7 @@ public function testNormalizeException14(): void $m = $m->createEntity(); $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Must be numeric'); $m->set('foo', 'ABC'); } diff --git a/tests/Persistence/Sql/ConnectionTest.php b/tests/Persistence/Sql/ConnectionTest.php index d2cab3ef8..0cb671ac0 100644 --- a/tests/Persistence/Sql/ConnectionTest.php +++ b/tests/Persistence/Sql/ConnectionTest.php @@ -8,34 +8,6 @@ use Atk4\Data\Exception; use Atk4\Data\Persistence; use Atk4\Data\Persistence\Sql\Connection; -use Doctrine\DBAL\Platforms\AbstractPlatform; -use Doctrine\DBAL\Platforms\SQLitePlatform; - -class DummyConnection extends Connection -{ - public function getDatabasePlatform(): AbstractPlatform - { - return new class() extends SQLitePlatform { - public function getName() - { - return 'dummy'; - } - }; - } -} - -class DummyConnection2 extends Connection -{ - public function getDatabasePlatform(): AbstractPlatform - { - return new class() extends SQLitePlatform { - public function getName() - { - return 'dummy2'; - } - }; - } -} class ConnectionTest extends TestCase { @@ -128,10 +100,15 @@ public function testDsnNormalize(): void public function testConnectionRegistry(): void { + $dummyConnectionClass = get_class(\Closure::bind(static fn () => new class() extends Connection {}, null, Connection::class)()); + $dummyConnectionClass2 = get_class(\Closure::bind(static fn () => new class() extends Connection {}, null, Connection::class)()); + self::assertNotSame($dummyConnectionClass, $dummyConnectionClass2); + $registryBackup = \Closure::bind(static fn () => Connection::$connectionClassRegistry, null, Connection::class)(); try { - Connection::registerConnectionClass(DummyConnection::class, 'dummy'); - self::assertSame(DummyConnection::class, Connection::resolveConnectionClass('dummy')); + Connection::registerConnectionClass($dummyConnectionClass, 'dummy'); + self::assertSame($dummyConnectionClass, Connection::resolveConnectionClass('dummy')); + try { Connection::resolveConnectionClass('dummy2'); self::assertFalse(true); // @phpstan-ignore-line @@ -139,8 +116,8 @@ public function testConnectionRegistry(): void self::assertSame('Driver schema is not registered', $e->getMessage()); } - Connection::registerConnectionClass(DummyConnection2::class, 'dummy2'); - self::assertSame(DummyConnection2::class, Connection::resolveConnectionClass('dummy2')); + Connection::registerConnectionClass($dummyConnectionClass2, 'dummy2'); + self::assertSame($dummyConnectionClass2, Connection::resolveConnectionClass('dummy2')); self::assertNotSame($registryBackup, \Closure::bind(static fn () => Connection::$connectionClassRegistry, null, Connection::class)()); } finally { @@ -169,14 +146,8 @@ public function testException2(): void public function testException3(): void { - $this->expectException(\TypeError::class); - new Persistence\Sql\Sqlite\Connection('sqlite::memory'); // @phpstan-ignore-line - } - - public function testException4(): void - { - $c = new Persistence\Sql\Sqlite\Connection(); - $q = $c->expr('select (2 + 2)'); + $connection = \Closure::bind(static fn () => new Persistence\Sql\Sqlite\Connection(), null, Connection::class)(); + $q = $connection->expr('select (2 + 2)'); self::assertSame('select (2 + 2)', $q->render()[0]); $this->expectException(Persistence\Sql\Exception::class); diff --git a/tests/Persistence/Sql/ExpressionTest.php b/tests/Persistence/Sql/ExpressionTest.php index b63b54701..f4a3b64d1 100644 --- a/tests/Persistence/Sql/ExpressionTest.php +++ b/tests/Persistence/Sql/ExpressionTest.php @@ -5,6 +5,7 @@ namespace Atk4\Data\Tests\Persistence\Sql; use Atk4\Core\Phpunit\TestCase; +use Atk4\Data\Persistence\Sql\Connection; use Atk4\Data\Persistence\Sql\Exception; use Atk4\Data\Persistence\Sql\Expression; use Atk4\Data\Persistence\Sql\Expressionable; @@ -212,7 +213,7 @@ public function testExpr(): void { self::assertInstanceOf(Expression::class, $this->e('foo')); - $connection = new Mysql\Connection(); + $connection = \Closure::bind(static fn () => new Mysql\Connection(), null, Connection::class)(); $e = new Mysql\Expression(['connection' => $connection]); self::assertSame(Mysql\Expression::class, get_class($e->expr('foo'))); self::assertSame($connection, $e->expr('foo')->connection); @@ -295,7 +296,7 @@ public function getDsqlExpression(Expression $expr): Expression } }; $e = $this->e('hello, []', [$myField]); - $e->connection = new Sqlite\Connection(); + $e->connection = \Closure::bind(static fn () => new Sqlite\Connection(), null, Connection::class)(); self::assertSame( 'hello, "myfield"', $e->render()[0] diff --git a/tests/Persistence/Sql/QueryTest.php b/tests/Persistence/Sql/QueryTest.php index 23b1b94b2..d249196a4 100644 --- a/tests/Persistence/Sql/QueryTest.php +++ b/tests/Persistence/Sql/QueryTest.php @@ -38,10 +38,12 @@ public function __construct($defaults = [], array $arguments = []) }; if (!(new \ReflectionProperty($query, 'connection'))->isInitialized($query)) { - $query->connection = new Persistence\Sql\Sqlite\Connection(); - \Closure::bind(static function () use ($query) { - $query->connection->expressionClass = \Closure::bind(static fn () => $query->expressionClass, null, Query::class)(); - $query->connection->queryClass = get_class($query); + $query->connection = \Closure::bind(static function () use ($query) { + $connection = new Persistence\Sql\Sqlite\Connection(); + $connection->expressionClass = \Closure::bind(static fn () => $query->expressionClass, null, Query::class)(); + $connection->queryClass = get_class($query); + + return $connection; }, null, Connection::class)(); } @@ -69,7 +71,7 @@ public function testExpr(): void { self::assertInstanceOf(Expression::class, $this->q()->expr('foo')); - $connection = new Mysql\Connection(); + $connection = \Closure::bind(static fn () => new Mysql\Connection(), null, Connection::class)(); $q = new Mysql\Query(['connection' => $connection]); self::assertSame(Mysql\Expression::class, get_class($q->expr('foo'))); self::assertSame($connection, $q->expr('foo')->connection); @@ -79,7 +81,7 @@ public function testDsql(): void { self::assertInstanceOf(Query::class, $this->q()->dsql()); - $connection = new Mysql\Connection(); + $connection = \Closure::bind(static fn () => new Mysql\Connection(), null, Connection::class)(); $q = new Mysql\Query(['connection' => $connection]); self::assertSame(Mysql\Query::class, get_class($q->dsql())); self::assertSame($connection, $q->dsql()->connection); diff --git a/tests/Schema/TestCaseTest.php b/tests/Schema/TestCaseTest.php index 999f5dd57..4482dcc90 100644 --- a/tests/Schema/TestCaseTest.php +++ b/tests/Schema/TestCaseTest.php @@ -40,11 +40,11 @@ public function testLogQuery(): void $makeLimitSqlFx = function (int $maxCount) { if ($this->getDatabasePlatform() instanceof PostgreSQLPlatform) { - return "\nlimit\n " . $maxCount . ' offset 0'; + return "\nlimit\n " . $maxCount . "\noffset\n 0"; } elseif ($this->getDatabasePlatform() instanceof SQLServerPlatform) { - return "\norder by\n (\n select\n null\n ) offset 0 rows fetch next " . $maxCount . ' rows only'; + return "\norder by\n (\n select\n null\n )\noffset\n 0 rows\nfetch\n next " . $maxCount . ' rows only'; } elseif ($this->getDatabasePlatform() instanceof OraclePlatform) { - return ' fetch next ' . $maxCount . ' rows only'; + return "\nfetch\n next " . $maxCount . ' rows only'; } return "\nlimit\n 0,\n " . $maxCount; diff --git a/tests/TypecastingTest.php b/tests/TypecastingTest.php index 46f11f28f..ec4c76f0d 100644 --- a/tests/TypecastingTest.php +++ b/tests/TypecastingTest.php @@ -5,10 +5,13 @@ namespace Atk4\Data\Tests; use Atk4\Data\Exception; +use Atk4\Data\Field; use Atk4\Data\Model; use Atk4\Data\Schema\TestCase; +use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\OraclePlatform; use Doctrine\DBAL\Platforms\SQLitePlatform; +use Doctrine\DBAL\Types as DbalTypes; class TypecastingTest extends TestCase { @@ -140,6 +143,7 @@ public function testEmptyValues(): void 'integer' => '', 'money' => '', 'float' => '', + 'decimal' => '', 'json' => '', 'object' => '', 'local-object' => '', @@ -160,6 +164,7 @@ public function testEmptyValues(): void $m->addField('integer', ['type' => 'integer']); $m->addField('money', ['type' => 'atk4_money']); $m->addField('float', ['type' => 'float']); + $m->addField('decimal', ['type' => 'decimal']); $m->addField('json', ['type' => 'json']); $m->addField('object', ['type' => 'object']); $m->addField('local-object', ['type' => 'atk4_local_object']); @@ -174,6 +179,7 @@ public function testEmptyValues(): void self::assertNull($mm->get('integer')); self::assertNull($mm->get('money')); self::assertNull($mm->get('float')); + self::assertNull($mm->get('decimal')); self::assertNull($mm->get('json')); self::assertNull($mm->get('object')); self::assertNull($mm->get('local-object')); @@ -191,6 +197,7 @@ public function testEmptyValues(): void self::assertNull($mm->get('integer')); self::assertNull($mm->get('money')); self::assertNull($mm->get('float')); + self::assertNull($mm->get('decimal')); self::assertNull($mm->get('json')); self::assertNull($mm->get('object')); self::assertNull($mm->get('local-object')); @@ -214,6 +221,7 @@ public function testEmptyValues(): void 'integer' => null, 'money' => null, 'float' => null, + 'decimal' => null, 'json' => null, 'object' => null, 'local-object' => null, @@ -246,6 +254,44 @@ public function testTypecastNull(): void self::{'assertEquals'}($dbData, $this->getDb()); } + /** + * @param \Closure(): void $fx + */ + protected function executeFxWithTemporaryType(string $name, DbalTypes\Type $type, \Closure $fx): void + { + $typeRegistry = DbalTypes\Type::getTypeRegistry(); + + $typeRegistry->register($name, $type); + try { + $fx(); + } finally { + \Closure::bind(static function () use ($typeRegistry, $name) { + unset($typeRegistry->instances[$name]); + }, null, DbalTypes\TypeRegistry::class)(); + } + } + + public function testSaveFieldUnexpectedScalarException(): void + { + $this->executeFxWithTemporaryType('bad-datetime', new class() extends DbalTypes\DateTimeType { + public function convertToDatabaseValue($value, AbstractPlatform $platform): \DateTime // @phpstan-ignore-line + { + return $value; + } + }, function () { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Unexpected non-scalar value'); + $this->db->typecastSaveField(new Field(['type' => 'bad-datetime']), new \DateTime()); + }); + } + + public function testLoadFieldUnexpectedScalarException(): void + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Unexpected non-scalar value'); + $this->db->typecastLoadField(new Field(['type' => 'datetime']), new \DateTime()); // @phpstan-ignore-line + } + public function testTypeCustom1(): void { $dbData = [