Skip to content

Commit 67c344e

Browse files
refactor: modernise the validation in active code and the associated tests when dealing with integer arrays (#308)
1 parent 1db35ac commit 67c344e

File tree

11 files changed

+300
-79
lines changed

11 files changed

+300
-79
lines changed

src/MartinGeorgiev/Doctrine/DBAL/Types/BaseIntegerArray.php

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
namespace MartinGeorgiev\Doctrine\DBAL\Types;
66

7-
use Doctrine\DBAL\Types\ConversionException;
7+
use MartinGeorgiev\Doctrine\DBAL\Types\Exceptions\InvalidIntegerArrayItemForDatabaseException;
8+
use MartinGeorgiev\Doctrine\DBAL\Types\Exceptions\InvalidIntegerArrayItemForPHPException;
89

910
/**
1011
* @see https://www.postgresql.org/docs/9.4/static/arrays.html
@@ -14,47 +15,77 @@
1415
*/
1516
abstract class BaseIntegerArray extends BaseArray
1617
{
17-
abstract protected function getMinValue(): string;
18+
private const INTEGER_REGEX = '/^-?\d+$/';
1819

19-
abstract protected function getMaxValue(): string;
20+
abstract protected function getMinValue(): int;
2021

21-
/**
22-
* @param mixed $item
23-
*/
24-
public function isValidArrayItemForDatabase($item): bool
22+
abstract protected function getMaxValue(): int;
23+
24+
public function isValidArrayItemForDatabase(mixed $item): bool
2525
{
26-
if (!\is_int($item) && !\is_string($item)) {
26+
try {
27+
$this->throwIfInvalidArrayItemForDatabase($item);
28+
} catch (InvalidIntegerArrayItemForDatabaseException) {
2729
return false;
2830
}
2931

30-
if (!(bool) \preg_match('/^-?\d+$/', (string) $item)) {
31-
return false;
32+
return true;
33+
}
34+
35+
private function throwIfInvalidArrayItemForDatabase(mixed $item): void
36+
{
37+
$isNotANumber = !\is_int($item) && !\is_string($item);
38+
if ($isNotANumber) {
39+
throw InvalidIntegerArrayItemForDatabaseException::isNotANumber($item);
3240
}
3341

34-
if ((string) $item < $this->getMinValue()) {
35-
return false;
42+
$stringValue = (string) $item;
43+
if (!\preg_match(self::INTEGER_REGEX, $stringValue)) {
44+
throw InvalidIntegerArrayItemForDatabaseException::doesNotMatchRegex($item);
45+
}
46+
47+
$doesNotFitIntoPHPInteger = $stringValue !== (string) (int) $stringValue;
48+
if ($doesNotFitIntoPHPInteger) {
49+
throw InvalidIntegerArrayItemForDatabaseException::isOutOfRange($item);
3650
}
3751

38-
return (string) $item <= $this->getMaxValue();
52+
$integerValue = (int) $item;
53+
if ($integerValue < $this->getMinValue()) {
54+
throw InvalidIntegerArrayItemForDatabaseException::isBelowMinValue($item);
55+
}
56+
57+
if ($integerValue > $this->getMaxValue()) {
58+
throw InvalidIntegerArrayItemForDatabaseException::isAboveMaxValue($item);
59+
}
3960
}
4061

41-
/**
42-
* @param int|string|null $item Whole number
43-
*/
44-
public function transformArrayItemForPHP($item): ?int
62+
public function transformArrayItemForPHP(mixed $item): ?int
4563
{
4664
if ($item === null) {
4765
return null;
4866
}
4967

68+
$isNotANumberCandidate = !\is_int($item) && !\is_string($item);
69+
if ($isNotANumberCandidate) {
70+
throw InvalidIntegerArrayItemForPHPException::forValueThatIsNotAValidPHPInteger($item, static::TYPE_NAME);
71+
}
72+
5073
$stringValue = (string) $item;
51-
$isInvalidPHPInt = !(bool) \preg_match('/^-?\d+$/', $stringValue)
52-
|| $stringValue < $this->getMinValue()
53-
|| $stringValue > $this->getMaxValue();
54-
if ($isInvalidPHPInt) {
55-
throw new ConversionException(\sprintf('Given value of %s content cannot be transformed to valid PHP integer.', \var_export($item, true)));
74+
if (!\preg_match(self::INTEGER_REGEX, $stringValue)) {
75+
throw InvalidIntegerArrayItemForPHPException::forValueThatIsNotAValidPHPInteger($item, static::TYPE_NAME);
76+
}
77+
78+
$doesNotFitIntoPHPInteger = $stringValue !== (string) (int) $stringValue;
79+
if ($doesNotFitIntoPHPInteger) {
80+
throw InvalidIntegerArrayItemForPHPException::forValueOutOfRangeInPHP($item, static::TYPE_NAME);
81+
}
82+
83+
$integerValue = (int) $item;
84+
$doesNotFitIntoDatabaseInteger = $integerValue < $this->getMinValue() || $integerValue > $this->getMaxValue();
85+
if ($doesNotFitIntoDatabaseInteger) {
86+
throw InvalidIntegerArrayItemForPHPException::forValueOutOfRangeInDatabaseType($item, static::TYPE_NAME);
5687
}
5788

58-
return (int) $item;
89+
return $integerValue;
5990
}
6091
}

src/MartinGeorgiev/Doctrine/DBAL/Types/BigIntArray.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ class BigIntArray extends BaseIntegerArray
1919
*/
2020
protected const TYPE_NAME = 'bigint[]';
2121

22-
protected function getMinValue(): string
22+
protected function getMinValue(): int
2323
{
24-
return '-9223372036854775807';
24+
return PHP_INT_MIN;
2525
}
2626

27-
protected function getMaxValue(): string
27+
protected function getMaxValue(): int
2828
{
29-
return '9223372036854775807';
29+
return PHP_INT_MAX;
3030
}
3131
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\DBAL\Types\Exceptions;
6+
7+
use Doctrine\DBAL\Types\ConversionException;
8+
9+
/**
10+
* @since 3.0
11+
*
12+
* @author Martin Georgiev <martin.georgiev@gmail.com>
13+
*/
14+
class InvalidIntegerArrayItemForDatabaseException extends ConversionException
15+
{
16+
private static function create(string $message, mixed $value): self
17+
{
18+
return new self(\sprintf($message, \var_export($value, true)));
19+
}
20+
21+
public static function isNotANumber(mixed $value): self
22+
{
23+
return self::create('Given value of %s is not a number.', $value);
24+
}
25+
26+
public static function doesNotMatchRegex(mixed $value): self
27+
{
28+
return self::create('Given value of %s does not match integer regex.', $value);
29+
}
30+
31+
public static function isBelowMinValue(mixed $value): self
32+
{
33+
return self::create('Given value of %s is below minimum value.', $value);
34+
}
35+
36+
public static function isAboveMaxValue(mixed $value): self
37+
{
38+
return self::create('Given value of %s is above maximum value.', $value);
39+
}
40+
41+
public static function isOutOfRange(mixed $value): self
42+
{
43+
return self::create('Given value of %s is out of range.', $value);
44+
}
45+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\DBAL\Types\Exceptions;
6+
7+
use Doctrine\DBAL\Types\ConversionException;
8+
9+
/**
10+
* @since 3.0
11+
*
12+
* @author Martin Georgiev <martin.georgiev@gmail.com>
13+
*/
14+
class InvalidIntegerArrayItemForPHPException extends ConversionException
15+
{
16+
private static function create(string $message, mixed $value, string $type): self
17+
{
18+
return new self(\sprintf($message, \var_export($value, true), $type));
19+
}
20+
21+
public static function forValueThatIsNotAValidPHPInteger(mixed $value, string $type): self
22+
{
23+
return self::create('Given value of %s content cannot be transformed to valid PHP integer from PostgreSQL %s type', $value, $type);
24+
}
25+
26+
public static function forValueOutOfRangeInPHP(mixed $value, string $type): self
27+
{
28+
return self::create('Given value of %s is out of range for PHP integer but appears valid for PostgreSQL %s type', $value, $type);
29+
}
30+
31+
public static function forValueOutOfRangeInDatabaseType(mixed $value, string $type): self
32+
{
33+
return self::create('Given value of %s is out of range for PostgreSQL %s type', $value, $type);
34+
}
35+
}

src/MartinGeorgiev/Doctrine/DBAL/Types/IntegerArray.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ class IntegerArray extends BaseIntegerArray
1919
*/
2020
protected const TYPE_NAME = 'integer[]';
2121

22-
protected function getMinValue(): string
22+
protected function getMinValue(): int
2323
{
24-
return '-2147483648';
24+
return -2147483648;
2525
}
2626

27-
protected function getMaxValue(): string
27+
protected function getMaxValue(): int
2828
{
29-
return '2147483647';
29+
return 2147483647;
3030
}
3131
}

src/MartinGeorgiev/Doctrine/DBAL/Types/SmallIntArray.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ class SmallIntArray extends BaseIntegerArray
1919
*/
2020
protected const TYPE_NAME = 'smallint[]';
2121

22-
protected function getMinValue(): string
22+
protected function getMinValue(): int
2323
{
24-
return '-32768';
24+
return -32768;
2525
}
2626

27-
protected function getMaxValue(): string
27+
protected function getMaxValue(): int
2828
{
29-
return '32767';
29+
return 32767;
3030
}
3131
}

tests/MartinGeorgiev/Doctrine/DBAL/Types/BaseIntegerArrayTestCase.php

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,11 @@
55
namespace Tests\MartinGeorgiev\Doctrine\DBAL\Types;
66

77
use MartinGeorgiev\Doctrine\DBAL\Types\BaseIntegerArray;
8-
use PHPUnit\Framework\MockObject\MockObject;
8+
use MartinGeorgiev\Doctrine\DBAL\Types\Exceptions\InvalidIntegerArrayItemForPHPException;
99
use PHPUnit\Framework\TestCase;
1010

1111
abstract class BaseIntegerArrayTestCase extends TestCase
1212
{
13-
/**
14-
* @var BaseIntegerArray&MockObject
15-
*/
1613
protected BaseIntegerArray $fixture;
1714

1815
/**
@@ -33,10 +30,14 @@ public static function provideInvalidTransformations(): array
3330
return [
3431
[true],
3532
[null],
36-
[-0.1],
3733
['string'],
3834
[[]],
3935
[new \stdClass()],
36+
['1.23'],
37+
['not_a_number'],
38+
['1e2'],
39+
['0xFF'],
40+
['123abc'],
4041
];
4142
}
4243

@@ -61,7 +62,28 @@ public function can_transform_to_php_value(int $phpValue, string $postgresValue)
6162
}
6263

6364
/**
64-
* @return list<array<string, int|string>>
65+
* @return list<array{
66+
* phpValue: int,
67+
* postgresValue: string
68+
* }>
6569
*/
6670
abstract public static function provideValidTransformations(): array;
71+
72+
/**
73+
* @test
74+
*
75+
* @dataProvider provideOutOfRangeValues
76+
*/
77+
public function throws_domain_exception_when_value_exceeds_range(string $outOfRangeValue): void
78+
{
79+
$this->expectException(InvalidIntegerArrayItemForPHPException::class);
80+
$this->expectExceptionMessage('is out of range for PostgreSQL');
81+
82+
$this->fixture->transformArrayItemForPHP($outOfRangeValue);
83+
}
84+
85+
/**
86+
* @return array<array{string}>
87+
*/
88+
abstract protected function provideOutOfRangeValues(): array;
6789
}

0 commit comments

Comments
 (0)