Skip to content
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
20 changes: 11 additions & 9 deletions docs/AVAILABLE-TYPES.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# Available types

| PostgreSQL type | Implemented by |
|---|---|
| _bool | `MartinGeorgiev\Doctrine\DBAL\Types\BooleanArray` |
| _int2 | `MartinGeorgiev\Doctrine\DBAL\Types\SmallIntArray` |
| _int4 | `MartinGeorgiev\Doctrine\DBAL\Types\IntegerArray` |
| _int8 | `MartinGeorgiev\Doctrine\DBAL\Types\BigIntArray` |
| _text | `MartinGeorgiev\Doctrine\DBAL\Types\TextArray` |
| _jsonb | `MartinGeorgiev\Doctrine\DBAL\Types\JsonbArray` |
| jsonb | `MartinGeorgiev\Doctrine\DBAL\Types\Jsonb` |
| PostgreSQL type in practical use | PostgreSQL internal system catalogue name | Implemented by |
|---|---|---|
| bool[] | _bool | `MartinGeorgiev\Doctrine\DBAL\Types\BooleanArray` |
| smallint[] | _int2 | `MartinGeorgiev\Doctrine\DBAL\Types\SmallIntArray` |
| integer[] | _int4 | `MartinGeorgiev\Doctrine\DBAL\Types\IntegerArray` |
| bigint[] | _int8 | `MartinGeorgiev\Doctrine\DBAL\Types\BigIntArray` |
| real[] | _float4 | `MartinGeorgiev\Doctrine\DBAL\Types\RealArray` |
| double precision[] | _float8 | `MartinGeorgiev\Doctrine\DBAL\Types\DoublePrecisionArray` |
| text[] | _text | `MartinGeorgiev\Doctrine\DBAL\Types\TextArray` |
| jsonb[] | _jsonb | `MartinGeorgiev\Doctrine\DBAL\Types\JsonbArray` |
| jsonb | jsonb | `MartinGeorgiev\Doctrine\DBAL\Types\Jsonb` |
128 changes: 128 additions & 0 deletions src/MartinGeorgiev/Doctrine/DBAL/Types/BaseFloatArray.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\DBAL\Types;

use MartinGeorgiev\Doctrine\DBAL\Types\Exceptions\InvalidFloatArrayItemForDatabaseException;
use MartinGeorgiev\Doctrine\DBAL\Types\Exceptions\InvalidFloatArrayItemForPHPException;

/**
* @since 3.0
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
abstract class BaseFloatArray extends BaseArray
{
private const FLOAT_REGEX = '/^-?\d*\.?\d+(?:[eE][-+]?\d+)?$/';

abstract protected function getMinValue(): string;

abstract protected function getMaxValue(): string;

abstract protected function getMaxPrecision(): int;

abstract protected function getMinAbsoluteValue(): string;

public function isValidArrayItemForDatabase(mixed $item): bool
{
try {
$this->throwIfInvalidArrayItemForDatabase($item);
} catch (InvalidFloatArrayItemForDatabaseException) {
return false;
}

return true;
}

private function throwIfInvalidArrayItemForDatabase(mixed $item): void
{
$isNotANumber = !\is_float($item) && !\is_int($item) && !\is_string($item);
if ($isNotANumber) {
throw InvalidFloatArrayItemForDatabaseException::isNotANumber($item);
}

$stringValue = (string) $item;
if (!\preg_match(self::FLOAT_REGEX, $stringValue)) {
throw InvalidFloatArrayItemForDatabaseException::doesNotMatchRegex($item);
}

$floatValue = (float) $stringValue;

// For scientific notation, convert to standard decimal form before checking precision
if (\str_contains($stringValue, 'e') || \str_contains($stringValue, 'E')) {
$standardForm = \sprintf('%.'.($this->getMaxPrecision() + 1).'f', $floatValue);
$parts = \explode('.', $standardForm);
if (isset($parts[1]) && \strlen($parts[1]) > $this->getMaxPrecision()) {
throw InvalidFloatArrayItemForDatabaseException::isAScientificNotationWithExcessPrecision($item);
}
} elseif (\str_contains($stringValue, '.')) {
$parts = \explode('.', $stringValue);
if (\strlen($parts[1]) > $this->getMaxPrecision()) {
throw InvalidFloatArrayItemForDatabaseException::isANormalNumberWithExcessPrecision($item);
}
}

$isBelowMinValue = $floatValue < (float) $this->getMinValue();
if ($isBelowMinValue) {
throw InvalidFloatArrayItemForDatabaseException::isBelowMinValue($item);
}

$isAboveMaxValue = $floatValue > (float) $this->getMaxValue();
if ($isAboveMaxValue) {
throw InvalidFloatArrayItemForDatabaseException::isAboveMaxValue($item);
}

// Check if value is too close to zero
$absoluteValue = \abs($floatValue);
$isTooCloseToZero = $absoluteValue > 0 && $absoluteValue < (float) $this->getMinAbsoluteValue();
if ($isTooCloseToZero) {
throw InvalidFloatArrayItemForDatabaseException::absoluteValueIsTooCloseToZero($item);
}
}

public function transformArrayItemForPHP(mixed $item): ?float
{
if ($item === null) {
return null;
}

$isNotANumberCandidate = !\is_float($item) && !\is_int($item) && !\is_string($item);
if ($isNotANumberCandidate) {
throw InvalidFloatArrayItemForPHPException::forValueThatIsNotAValidPHPFloat($item, static::TYPE_NAME);
}

$stringValue = (string) $item;
if (!\preg_match(self::FLOAT_REGEX, $stringValue)) {
throw InvalidFloatArrayItemForPHPException::forValueThatIsNotAValidPHPFloat($item, static::TYPE_NAME);
}

$floatValue = (float) $stringValue;

// Check if value is too close to zero
$absValue = \abs($floatValue);
if ($absValue > 0 && $absValue < (float) $this->getMinAbsoluteValue()) {
throw InvalidFloatArrayItemForPHPException::forValueThatIsTooCloseToZero($item, static::TYPE_NAME);
}

if ($floatValue < (float) $this->getMinValue() || $floatValue > (float) $this->getMaxValue()) {
throw InvalidFloatArrayItemForPHPException::forValueThatIsNotAValidPHPFloat($item, static::TYPE_NAME);
}

// Scientific notation is valid for input as long as the resulting number
// when converted to decimal doesn't exceed precision limits
if (\str_contains($stringValue, 'e') || \str_contains($stringValue, 'E')) {
return $floatValue;
}

// For regular decimal notation, check precision
if (\str_contains($stringValue, '.')) {
$parts = \explode('.', $stringValue);
if (\strlen($parts[1]) > $this->getMaxPrecision()) {
throw InvalidFloatArrayItemForPHPException::forValueThatExceedsMaximumPrecision($item, static::TYPE_NAME);
}
}

return $floatValue;
}
}
38 changes: 38 additions & 0 deletions src/MartinGeorgiev/Doctrine/DBAL/Types/DoublePrecisionArray.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\DBAL\Types;

/**
* Implementation of PostgreSQL DOUBLE PRECISION[] data type.
*
* @see https://www.postgresql.org/docs/current/datatype-numeric.html
* @since 3.0
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
class DoublePrecisionArray extends BaseFloatArray
{
protected const TYPE_NAME = 'double precision[]';

protected function getMinValue(): string
{
return '-1.7976931348623157E+308';
}

protected function getMaxValue(): string
{
return '1.7976931348623157E+308';
}

protected function getMaxPrecision(): int
{
return 15;
}

protected function getMinAbsoluteValue(): string
{
return '2.2250738585072014E-308';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\DBAL\Types\Exceptions;

use Doctrine\DBAL\Types\ConversionException;

/**
* @since 3.0
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
class InvalidFloatArrayItemForDatabaseException extends ConversionException
{
private static function create(string $message, mixed $value): self
{
return new self(\sprintf($message, \var_export($value, true)));
}

public static function isNotANumber(mixed $value): self
{
return self::create('Given value of %s is not a number.', $value);
}

public static function doesNotMatchRegex(mixed $value): self
{
return self::create('Given value of %s does not match float regex.', $value);
}

public static function isAScientificNotationWithExcessPrecision(mixed $value): self
{
return self::create('Given value of %s is a scientific notation with excess precision.', $value);
}

public static function isANormalNumberWithExcessPrecision(mixed $value): self
{
return self::create('Given value of %s is a normal number with excess precision.', $value);
}

public static function isBelowMinValue(mixed $value): self
{
return self::create('Given value of %s is below minimum value.', $value);
}

public static function isAboveMaxValue(mixed $value): self
{
return self::create('Given value of %s is above maximum value.', $value);
}

public static function absoluteValueIsTooCloseToZero(mixed $value): self
{
return self::create('Given absolute value of %s is too close to zero.', $value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\DBAL\Types\Exceptions;

use Doctrine\DBAL\Types\ConversionException;

/**
* @since 3.0
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
class InvalidFloatArrayItemForPHPException extends ConversionException
{
private static function create(string $message, mixed $value, string $type): self
{
return new self(\sprintf($message, \var_export($value, true), $type));
}

public static function forValueThatIsNotAValidPHPFloat(mixed $value, string $type): self
{
return self::create('Given value of %s content cannot be transformed to valid PHP float from PostgreSQL %s type', $value, $type);
}

public static function forValueThatIsTooCloseToZero(mixed $value, string $type): self
{
return self::create('Given value of %s is too close to zero for PostgreSQL %s type', $value, $type);
}

public static function forValueThatExceedsMaximumPrecision(mixed $value, string $type): self
{
return self::create('Given value of %s exceeds maximum precision for PostgreSQL %s type', $value, $type);
}
}
38 changes: 38 additions & 0 deletions src/MartinGeorgiev/Doctrine/DBAL/Types/RealArray.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\DBAL\Types;

/**
* Implementation of PostgreSQL REAL[] data type.
*
* @see https://www.postgresql.org/docs/current/datatype-numeric.html
* @since 3.0
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
class RealArray extends BaseFloatArray
{
protected const TYPE_NAME = 'real[]';

protected function getMinValue(): string
{
return '-3.4028235E+38';
}

protected function getMaxValue(): string
{
return '3.4028235E+38';
}

protected function getMaxPrecision(): int
{
return 6;
}

protected function getMinAbsoluteValue(): string
{
return '1.17549435E-38';
}
}
4 changes: 2 additions & 2 deletions tests/MartinGeorgiev/Doctrine/DBAL/Types/BaseArrayTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public function throws_invalid_argument_exception_when_php_value_is_not_array():
/**
* @test
*/
public function throws_conversion_exception_when_invalid_array_item_value(): void
public function throws_domain_exception_when_invalid_array_item_value(): void
{
$this->expectException(ConversionException::class);
$this->expectExceptionMessage("One or more of the items given doesn't look valid.");
Expand All @@ -110,7 +110,7 @@ public function throws_conversion_exception_when_invalid_array_item_value(): voi
/**
* @test
*/
public function throws_conversion_exception_when_postgres_value_is_not_valid_php_array(): void
public function throws_domain_exception_when_postgres_value_is_not_valid_php_array(): void
{
$this->expectException(ConversionException::class);
$this->expectExceptionMessageMatches('/Given PostgreSQL value content type is not PHP string. Instead it is "\w+"./');
Expand Down
Loading
Loading