Skip to content

Commit 7192b19

Browse files
committed
Add NumberType
1 parent 4d8badd commit 7192b19

File tree

8 files changed

+227
-8
lines changed

8 files changed

+227
-8
lines changed

.github/workflows/static-analysis.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
strategy:
3535
matrix:
3636
php-version:
37-
- "8.3"
37+
- "8.4"
3838

3939
steps:
4040
- name: "Checkout code"
@@ -49,6 +49,8 @@ jobs:
4949

5050
- name: "Install dependencies with Composer"
5151
uses: "ramsey/composer-install@v3"
52+
with:
53+
composer-options: "--ignore-platform-req=php+"
5254

5355
- name: "Run a static analysis with phpstan/phpstan"
5456
run: "vendor/bin/phpstan --error-format=checkstyle | cs2pr"
@@ -60,7 +62,7 @@ jobs:
6062
strategy:
6163
matrix:
6264
php-version:
63-
- "8.3"
65+
- "8.4"
6466

6567
steps:
6668
- name: Checkout code
@@ -75,6 +77,8 @@ jobs:
7577

7678
- name: Install dependencies with Composer
7779
uses: ramsey/composer-install@v3
80+
with:
81+
composer-options: "--ignore-platform-req=php+"
7882

7983
- name: Run static analysis with Vimeo Psalm
8084
run: vendor/bin/psalm --shepherd

src/Types/NumberType.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Types;
6+
7+
use BcMath\Number;
8+
use Doctrine\DBAL\Platforms\AbstractPlatform;
9+
use Doctrine\DBAL\Types\Exception\InvalidType;
10+
use Doctrine\DBAL\Types\Exception\ValueNotConvertible;
11+
use TypeError;
12+
use ValueError;
13+
14+
use function is_float;
15+
16+
final class NumberType extends Type
17+
{
18+
/** {@inheritDoc} */
19+
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
20+
{
21+
return $platform->getDecimalTypeDeclarationSQL($column);
22+
}
23+
24+
public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string
25+
{
26+
if ($value === null) {
27+
return null;
28+
}
29+
30+
if (! $value instanceof Number) {
31+
throw InvalidType::new($value, static::class, ['null', Number::class]);
32+
}
33+
34+
return (string) $value;
35+
}
36+
37+
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?Number
38+
{
39+
if ($value === null) {
40+
return null;
41+
}
42+
43+
// SQLite might return a decimal as float.
44+
if (is_float($value)) {
45+
$value = (string) $value;
46+
}
47+
48+
try {
49+
return new Number($value);
50+
} catch (TypeError | ValueError $e) {
51+
throw ValueNotConvertible::new($value, static::class, previous: $e);
52+
}
53+
}
54+
}

src/Types/Type.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ abstract class Type
3434
Types::DATETIMETZ_MUTABLE => DateTimeTzType::class,
3535
Types::DATETIMETZ_IMMUTABLE => DateTimeTzImmutableType::class,
3636
Types::DECIMAL => DecimalType::class,
37+
Types::NUMBER => NumberType::class,
3738
Types::ENUM => EnumType::class,
3839
Types::FLOAT => FloatType::class,
3940
Types::GUID => GuidType::class,

src/Types/Types.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ final class Types
2222
public const DATETIMETZ_MUTABLE = 'datetimetz';
2323
public const DATETIMETZ_IMMUTABLE = 'datetimetz_immutable';
2424
public const DECIMAL = 'decimal';
25+
public const NUMBER = 'number';
2526
public const FLOAT = 'float';
2627
public const ENUM = 'enum';
2728
public const GUID = 'guid';

tests/Functional/Platform/AlterDecimalColumnTest.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
use Doctrine\DBAL\Types\Types;
1010
use PHPUnit\Framework\Attributes\DataProvider;
1111

12+
use function sprintf;
13+
1214
class AlterDecimalColumnTest extends FunctionalTestCase
1315
{
1416
#[DataProvider('scaleAndPrecisionProvider')]
15-
public function testAlterPrecisionAndScale(int $newPrecision, int $newScale): void
17+
public function testAlterPrecisionAndScale(int $newPrecision, int $newScale, string $type): void
1618
{
1719
$table = new Table('decimal_table');
18-
$column = $table->addColumn('val', Types::DECIMAL, ['precision' => 16, 'scale' => 6]);
20+
$column = $table->addColumn('val', $type, ['precision' => 16, 'scale' => 6]);
1921

2022
$this->dropAndCreateTable($table);
2123

@@ -36,11 +38,13 @@ public function testAlterPrecisionAndScale(int $newPrecision, int $newScale): vo
3638
self::assertSame($newScale, $column->getScale());
3739
}
3840

39-
/** @return iterable<string,array{int,int}> */
41+
/** @return iterable<string,array{int,int,Types::*}> */
4042
public static function scaleAndPrecisionProvider(): iterable
4143
{
42-
yield 'Precision' => [12, 6];
43-
yield 'Scale' => [16, 8];
44-
yield 'Precision and scale' => [10, 4];
44+
foreach ([Types::DECIMAL, Types::NUMBER] as $type) {
45+
yield sprintf('Precision (%s)', $type) => [12, 6, $type];
46+
yield sprintf('Scale (%s)', $type) => [16, 8, $type];
47+
yield sprintf('Precision and scale (%s)', $type) => [10, 4, $type];
48+
}
4549
}
4650
}

tests/Functional/TypeConversionTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44

55
namespace Doctrine\DBAL\Tests\Functional;
66

7+
use BcMath\Number;
78
use DateTime;
89
use Doctrine\DBAL\Schema\Table;
910
use Doctrine\DBAL\Tests\FunctionalTestCase;
1011
use Doctrine\DBAL\Tests\TestUtil;
1112
use Doctrine\DBAL\Types\Type;
1213
use Doctrine\DBAL\Types\Types;
1314
use PHPUnit\Framework\Attributes\DataProvider;
15+
use PHPUnit\Framework\Attributes\RequiresPhp;
16+
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
1417

1518
use function str_repeat;
1619

@@ -38,6 +41,7 @@ protected function setUp(): void
3841
$table->addColumn('test_float', Types::FLOAT, ['notnull' => false]);
3942
$table->addColumn('test_smallfloat', Types::SMALLFLOAT, ['notnull' => false]);
4043
$table->addColumn('test_decimal', Types::DECIMAL, ['notnull' => false, 'scale' => 2, 'precision' => 10]);
44+
$table->addColumn('test_number', Types::NUMBER, ['notnull' => false, 'scale' => 2, 'precision' => 10]);
4145
$table->setPrimaryKey(['id']);
4246

4347
$this->dropAndCreateTable($table);
@@ -154,6 +158,22 @@ public static function toDateTimeProvider(): iterable
154158
];
155159
}
156160

161+
public function testDecimal(): void
162+
{
163+
self::assertSame('13.37', $this->processValue(Types::DECIMAL, '13.37'));
164+
}
165+
166+
#[RequiresPhp('8.4')]
167+
168+
#[RequiresPhpExtension('bcmath')]
169+
public function testNumber(): void
170+
{
171+
$originalValue = new Number('13.37');
172+
$dbValue = $this->processValue(Types::NUMBER, $originalValue);
173+
174+
self::assertSame(0, $originalValue->compare($dbValue));
175+
}
176+
157177
private function processValue(string $type, mixed $originalValue): mixed
158178
{
159179
$columnName = 'test_' . $type;

tests/Functional/Types/NumberTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Tests\Functional\Types;
6+
7+
use BcMath\Number;
8+
use Doctrine\DBAL\Schema\Table;
9+
use Doctrine\DBAL\Tests\FunctionalTestCase;
10+
use Doctrine\DBAL\Types\Types;
11+
use PHPUnit\Framework\Attributes\RequiresPhp;
12+
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
13+
use PHPUnit\Framework\Attributes\TestWith;
14+
15+
#[RequiresPhp('8.4')]
16+
#[RequiresPhpExtension('bcmath')]
17+
final class NumberTest extends FunctionalTestCase
18+
{
19+
#[TestWith(['13.37'])]
20+
#[TestWith(['13.0'])]
21+
public function testInsertAndRetrieveNumber(string $numberAsString): void
22+
{
23+
$expected = new Number($numberAsString);
24+
25+
$table = new Table('number_table');
26+
$table->addColumn('val', Types::NUMBER, ['precision' => 4, 'scale' => 2]);
27+
28+
$this->dropAndCreateTable($table);
29+
30+
$this->connection->insert(
31+
'number_table',
32+
['val' => $expected],
33+
['val' => Types::NUMBER],
34+
);
35+
36+
$value = $this->connection->convertToPHPValue(
37+
$this->connection->fetchOne('SELECT val FROM number_table'),
38+
Types::NUMBER,
39+
);
40+
41+
self::assertInstanceOf(Number::class, $value);
42+
self::assertSame(0, $expected <=> $value);
43+
}
44+
45+
public function testCompareNumberTable(): void
46+
{
47+
$table = new Table('number_table');
48+
$table->addColumn('val', Types::NUMBER, ['precision' => 4, 'scale' => 2]);
49+
50+
$this->dropAndCreateTable($table);
51+
52+
$schemaManager = $this->connection->createSchemaManager();
53+
54+
self::assertTrue(
55+
$schemaManager->createComparator()
56+
->compareTables($schemaManager->introspectTable('number_table'), $table)
57+
->isEmpty(),
58+
);
59+
}
60+
}

tests/Types/NumberTest.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Tests\Types;
6+
7+
use BcMath\Number;
8+
use Doctrine\DBAL\Platforms\AbstractPlatform;
9+
use Doctrine\DBAL\Types\Exception\InvalidType;
10+
use Doctrine\DBAL\Types\Exception\ValueNotConvertible;
11+
use Doctrine\DBAL\Types\NumberType;
12+
use PHPUnit\Framework\Attributes\RequiresPhp;
13+
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
14+
use PHPUnit\Framework\Attributes\TestWith;
15+
use PHPUnit\Framework\MockObject\MockObject;
16+
use PHPUnit\Framework\TestCase;
17+
use stdClass;
18+
19+
#[RequiresPhp('8.4')]
20+
#[RequiresPhpExtension('bcmath')]
21+
final class NumberTest extends TestCase
22+
{
23+
private AbstractPlatform&MockObject $platform;
24+
private NumberType $type;
25+
26+
protected function setUp(): void
27+
{
28+
$this->platform = $this->createMock(AbstractPlatform::class);
29+
$this->type = new NumberType();
30+
}
31+
32+
#[TestWith(['5.5'])]
33+
#[TestWith(['5.5000'])]
34+
#[TestWith([5.5])]
35+
public function testDecimalConvertsToPHPValue(mixed $dbValue): void
36+
{
37+
$phpValue = $this->type->convertToPHPValue($dbValue, $this->platform);
38+
39+
self::assertInstanceOf(Number::class, $phpValue);
40+
self::assertSame(0, $phpValue->compare(new Number('5.5')));
41+
}
42+
43+
public function testDecimalNullConvertsToPHPValue(): void
44+
{
45+
self::assertNull($this->type->convertToPHPValue(null, $this->platform));
46+
}
47+
48+
public function testNumberConvertsToDecimalString(): void
49+
{
50+
self::assertSame('5.5', $this->type->convertToDatabaseValue(new Number('5.5'), $this->platform));
51+
}
52+
53+
public function testNumberNullConvertsToNull(): void
54+
{
55+
self::assertNull($this->type->convertToDatabaseValue(null, $this->platform));
56+
}
57+
58+
#[TestWith(['5.5'])]
59+
#[TestWith([new stdClass()])]
60+
public function testInvalidPhpValuesTriggerException(mixed $value): void
61+
{
62+
self::expectException(InvalidType::class);
63+
64+
$this->type->convertToDatabaseValue($value, $this->platform);
65+
}
66+
67+
#[TestWith(['foo'])]
68+
#[TestWith([true])]
69+
public function testUnexpectedValuesReturnedByTheDatabaseTriggerException(mixed $value): void
70+
{
71+
self::expectException(ValueNotConvertible::class);
72+
73+
$this->type->convertToPHPValue($value, $this->platform);
74+
}
75+
}

0 commit comments

Comments
 (0)