Skip to content

Add a NumberType that maps to the BcMath\Number value object #6686

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
strategy:
matrix:
php-version:
- "8.3"
- "8.4"

steps:
- name: "Checkout code"
Expand All @@ -49,6 +49,8 @@ jobs:

- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v3"
with:
composer-options: "--ignore-platform-req=php+"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is necessary because of Psalm, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I know, we need to kick it.


- name: "Run a static analysis with phpstan/phpstan"
run: "vendor/bin/phpstan --error-format=checkstyle | cs2pr"
Expand All @@ -60,7 +62,7 @@ jobs:
strategy:
matrix:
php-version:
- "8.3"
- "8.4"

steps:
- name: Checkout code
Expand All @@ -75,6 +77,8 @@ jobs:

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

- name: Run static analysis with Vimeo Psalm
run: vendor/bin/psalm --shepherd
Expand Down
6 changes: 3 additions & 3 deletions docs/en/reference/schema-representation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,11 @@ The following options are considered to be fully portable across all database pl
in the platform.
- **fixed** (boolean): Whether a ``string`` or ``binary`` Doctrine type column has
a fixed length. Defaults to ``false``.
- **precision** (integer): The precision of a Doctrine ``decimal`` or ``float`` type
column that determines the overall maximum number of digits to be stored (including scale).
- **precision** (integer): The precision of a Doctrine ``decimal``, ``number`` or ``float``
type column that determines the overall maximum number of digits to be stored (including scale).
Defaults to ``10``.
- **scale** (integer): The exact number of decimal digits to be stored in a Doctrine
``decimal`` or ``float`` type column. Defaults to ``0``.
``decimal``, ``number`` or ``float`` type column. Defaults to ``0``.
- **customSchemaOptions** (array): Additional options for the column that are
supported by all vendors:

Expand Down
449 changes: 233 additions & 216 deletions docs/en/reference/types.rst

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions src/Types/NumberType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Types;

use BcMath\Number;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Exception\InvalidType;
use Doctrine\DBAL\Types\Exception\ValueNotConvertible;
use TypeError;
use ValueError;

use function is_float;

final class NumberType extends Type
{
/** {@inheritDoc} */
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
return $platform->getDecimalTypeDeclarationSQL($column);
}

public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string
{
if ($value === null) {
return null;
}

if (! $value instanceof Number) {
throw InvalidType::new($value, static::class, ['null', Number::class]);
}

return (string) $value;
}

public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?Number
{
if ($value === null) {
return null;
}

// SQLite might return a decimal as float.
if (is_float($value)) {
$value = (string) $value;
}

try {
return new Number($value);
} catch (TypeError | ValueError $e) {
throw ValueNotConvertible::new($value, static::class, previous: $e);
}
}
}
1 change: 1 addition & 0 deletions src/Types/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ abstract class Type
Types::DATETIMETZ_MUTABLE => DateTimeTzType::class,
Types::DATETIMETZ_IMMUTABLE => DateTimeTzImmutableType::class,
Types::DECIMAL => DecimalType::class,
Types::NUMBER => NumberType::class,
Types::ENUM => EnumType::class,
Types::FLOAT => FloatType::class,
Types::GUID => GuidType::class,
Expand Down
1 change: 1 addition & 0 deletions src/Types/Types.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ final class Types
public const DATETIMETZ_MUTABLE = 'datetimetz';
public const DATETIMETZ_IMMUTABLE = 'datetimetz_immutable';
public const DECIMAL = 'decimal';
public const NUMBER = 'number';
public const FLOAT = 'float';
public const ENUM = 'enum';
public const GUID = 'guid';
Expand Down
16 changes: 10 additions & 6 deletions tests/Functional/Platform/AlterDecimalColumnTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
use Doctrine\DBAL\Types\Types;
use PHPUnit\Framework\Attributes\DataProvider;

use function sprintf;

class AlterDecimalColumnTest extends FunctionalTestCase
{
#[DataProvider('scaleAndPrecisionProvider')]
public function testAlterPrecisionAndScale(int $newPrecision, int $newScale): void
public function testAlterPrecisionAndScale(int $newPrecision, int $newScale, string $type): void
{
$table = new Table('decimal_table');
$column = $table->addColumn('val', Types::DECIMAL, ['precision' => 16, 'scale' => 6]);
$column = $table->addColumn('val', $type, ['precision' => 16, 'scale' => 6]);

$this->dropAndCreateTable($table);

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

/** @return iterable<string,array{int,int}> */
/** @return iterable<string,array{int,int,Types::*}> */
public static function scaleAndPrecisionProvider(): iterable
{
yield 'Precision' => [12, 6];
yield 'Scale' => [16, 8];
yield 'Precision and scale' => [10, 4];
foreach ([Types::DECIMAL, Types::NUMBER] as $type) {
yield sprintf('Precision (%s)', $type) => [12, 6, $type];
yield sprintf('Scale (%s)', $type) => [16, 8, $type];
yield sprintf('Precision and scale (%s)', $type) => [10, 4, $type];
}
}
}
19 changes: 19 additions & 0 deletions tests/Functional/TypeConversionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

namespace Doctrine\DBAL\Tests\Functional;

use BcMath\Number;
use DateTime;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Tests\FunctionalTestCase;
use Doctrine\DBAL\Tests\TestUtil;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;

use function str_repeat;

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

$this->dropAndCreateTable($table);
Expand Down Expand Up @@ -154,6 +158,21 @@ public static function toDateTimeProvider(): iterable
];
}

public function testDecimal(): void
{
self::assertSame('13.37', $this->processValue(Types::DECIMAL, '13.37'));
}

#[RequiresPhp('8.4')]
#[RequiresPhpExtension('bcmath')]
public function testNumber(): void
{
$originalValue = new Number('13.37');
$dbValue = $this->processValue(Types::NUMBER, $originalValue);

self::assertSame(0, $originalValue <=> $dbValue);
}

private function processValue(string $type, mixed $originalValue): mixed
{
$columnName = 'test_' . $type;
Expand Down
60 changes: 60 additions & 0 deletions tests/Functional/Types/NumberTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Tests\Functional\Types;

use BcMath\Number;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Tests\FunctionalTestCase;
use Doctrine\DBAL\Types\Types;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\Attributes\TestWith;

#[RequiresPhp('8.4')]
#[RequiresPhpExtension('bcmath')]
final class NumberTest extends FunctionalTestCase
{
#[TestWith(['13.37'])]
#[TestWith(['13.0'])]
public function testInsertAndRetrieveNumber(string $numberAsString): void
{
$expected = new Number($numberAsString);

$table = new Table('number_table');
$table->addColumn('val', Types::NUMBER, ['precision' => 4, 'scale' => 2]);

$this->dropAndCreateTable($table);

$this->connection->insert(
'number_table',
['val' => $expected],
['val' => Types::NUMBER],
);

$value = $this->connection->convertToPHPValue(
$this->connection->fetchOne('SELECT val FROM number_table'),
Types::NUMBER,
);

self::assertInstanceOf(Number::class, $value);
self::assertSame(0, $expected <=> $value);
}

public function testCompareNumberTable(): void
{
$table = new Table('number_table');
$table->addColumn('val', Types::NUMBER, ['precision' => 4, 'scale' => 2]);

$this->dropAndCreateTable($table);

$schemaManager = $this->connection->createSchemaManager();

self::assertTrue(
$schemaManager->createComparator()
->compareTables($schemaManager->introspectTable('number_table'), $table)
->isEmpty(),
);
}
}
75 changes: 75 additions & 0 deletions tests/Types/NumberTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Tests\Types;

use BcMath\Number;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Exception\InvalidType;
use Doctrine\DBAL\Types\Exception\ValueNotConvertible;
use Doctrine\DBAL\Types\NumberType;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use stdClass;

#[RequiresPhp('8.4')]
#[RequiresPhpExtension('bcmath')]
final class NumberTest extends TestCase
{
private AbstractPlatform&MockObject $platform;
private NumberType $type;

protected function setUp(): void
{
$this->platform = $this->createMock(AbstractPlatform::class);
$this->type = new NumberType();
}

#[TestWith(['5.5'])]
#[TestWith(['5.5000'])]
#[TestWith([5.5])]
public function testDecimalConvertsToPHPValue(mixed $dbValue): void
{
$phpValue = $this->type->convertToPHPValue($dbValue, $this->platform);

self::assertInstanceOf(Number::class, $phpValue);
self::assertSame(0, $phpValue <=> new Number('5.5'));
}

public function testDecimalNullConvertsToPHPValue(): void
{
self::assertNull($this->type->convertToPHPValue(null, $this->platform));
}

public function testNumberConvertsToDecimalString(): void
{
self::assertSame('5.5', $this->type->convertToDatabaseValue(new Number('5.5'), $this->platform));
}

public function testNumberNullConvertsToNull(): void
{
self::assertNull($this->type->convertToDatabaseValue(null, $this->platform));
}

#[TestWith(['5.5'])]
#[TestWith([new stdClass()])]
public function testInvalidPhpValuesTriggerException(mixed $value): void
{
self::expectException(InvalidType::class);

$this->type->convertToDatabaseValue($value, $this->platform);
}

#[TestWith(['foo'])]
#[TestWith([true])]
public function testUnexpectedValuesReturnedByTheDatabaseTriggerException(mixed $value): void
{
self::expectException(ValueNotConvertible::class);

$this->type->convertToPHPValue($value, $this->platform);
}
}