Skip to content

Commit ff71701

Browse files
committed
Map database column types to PHPStan types
1 parent e425d6b commit ff71701

2 files changed

Lines changed: 133 additions & 0 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\PHPStan\Database\Schema;
15+
16+
use PHPStan\Type\FloatType;
17+
use PHPStan\Type\IntegerType;
18+
use PHPStan\Type\StringType;
19+
use PHPStan\Type\Type;
20+
use PHPStan\Type\TypeCombinator;
21+
22+
/**
23+
* Maps an introspected column to the PHPStan type of its raw, uncast database value.
24+
*/
25+
final class ColumnTypeResolver
26+
{
27+
public function resolve(Column $column): Type
28+
{
29+
$type = $this->mapDeclaredType($column->type);
30+
31+
return $column->nullable ? TypeCombinator::addNull($type) : $type;
32+
}
33+
34+
private function mapDeclaredType(string $declaredType): Type
35+
{
36+
$declaredType = strtoupper($declaredType);
37+
38+
// Checks follow SQLite's column affinity precedence.
39+
// https://www.sqlite.org/datatype3.html#determination_of_column_affinity
40+
if (str_contains($declaredType, 'INT')) {
41+
return new IntegerType();
42+
}
43+
44+
if (
45+
str_contains($declaredType, 'CHAR')
46+
|| str_contains($declaredType, 'CLOB')
47+
|| str_contains($declaredType, 'TEXT')
48+
|| str_contains($declaredType, 'BLOB')
49+
|| $declaredType === ''
50+
) {
51+
return new StringType();
52+
}
53+
54+
if (
55+
str_contains($declaredType, 'REAL')
56+
|| str_contains($declaredType, 'FLOA')
57+
|| str_contains($declaredType, 'DOUB')
58+
) {
59+
return new FloatType();
60+
}
61+
62+
// NUMERIC affinity (DECIMAL, NUMERIC, DATE, DATETIME, ...), read back as strings without a cast.
63+
return new StringType();
64+
}
65+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\PHPStan\Tests\Database\Schema;
15+
16+
use CodeIgniter\PHPStan\Database\Schema\Column;
17+
use CodeIgniter\PHPStan\Database\Schema\ColumnTypeResolver;
18+
use PHPStan\Type\VerbosityLevel;
19+
use PHPUnit\Framework\Attributes\DataProvider;
20+
use PHPUnit\Framework\Attributes\Group;
21+
use PHPUnit\Framework\TestCase;
22+
23+
/**
24+
* @internal
25+
*/
26+
#[Group('unit')]
27+
final class ColumnTypeResolverTest extends TestCase
28+
{
29+
#[DataProvider('provideMapsDeclaredTypeToPhpStanTypeCases')]
30+
public function testMapsDeclaredTypeToPhpStanType(string $declaredType, bool $nullable, string $expected): void
31+
{
32+
$type = (new ColumnTypeResolver())->resolve(new Column('column', $declaredType, $nullable, false, null));
33+
34+
self::assertSame($expected, $type->describe(VerbosityLevel::precise()));
35+
}
36+
37+
/**
38+
* @return iterable<string, array{string, bool, string}>
39+
*/
40+
public static function provideMapsDeclaredTypeToPhpStanTypeCases(): iterable
41+
{
42+
yield 'INTEGER' => ['INTEGER', false, 'int'];
43+
44+
yield 'INT' => ['INT', false, 'int'];
45+
46+
yield 'BIGINT' => ['BIGINT', false, 'int'];
47+
48+
yield 'VARCHAR' => ['VARCHAR', false, 'string'];
49+
50+
yield 'TEXT' => ['TEXT', false, 'string'];
51+
52+
yield 'BLOB' => ['BLOB', false, 'string'];
53+
54+
yield 'REAL' => ['REAL', false, 'float'];
55+
56+
yield 'FLOAT' => ['FLOAT', false, 'float'];
57+
58+
yield 'DOUBLE' => ['DOUBLE', false, 'float'];
59+
60+
yield 'DATETIME (numeric -> string)' => ['DATETIME', false, 'string'];
61+
62+
yield 'DECIMAL (numeric -> string)' => ['DECIMAL', false, 'string'];
63+
64+
yield 'nullable INTEGER' => ['INTEGER', true, 'int|null'];
65+
66+
yield 'nullable VARCHAR' => ['VARCHAR', true, 'string|null'];
67+
}
68+
}

0 commit comments

Comments
 (0)