Skip to content

Commit 1c078f5

Browse files
authored
Merge pull request #253 from RonasIT/252-add-db-type-range-validation-rule
[252]: add DB type range validation rule
2 parents 83cb996 + 8d05e57 commit 1c078f5

8 files changed

Lines changed: 646 additions & 0 deletions

File tree

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
}
1111
],
1212
"require": {
13+
"ext-bcmath": "*",
1314
"ext-json": "*",
1415
"guzzlehttp/guzzle": "^7.9.2",
1516
"laravel/framework": ">=11.0",
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace RonasIT\Support\Contracts;
4+
5+
use RonasIT\Support\Enums\DBTypeCategoryEnum;
6+
7+
interface DBTypeResolverContract
8+
{
9+
/**
10+
* @return array{0: int|float, 1: int|float}
11+
*/
12+
public function getRange(string $type): array;
13+
14+
public function hasType(string $type): bool;
15+
16+
public function getTypeCategory(string $type): ?DBTypeCategoryEnum;
17+
}

src/Enums/DBTypeCategoryEnum.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace RonasIT\Support\Enums;
4+
5+
enum DBTypeCategoryEnum
6+
{
7+
case Integer;
8+
case Float;
9+
case String;
10+
}

src/HelpersServiceProvider.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414
use Illuminate\Support\Pluralizer;
1515
use Illuminate\Support\ServiceProvider;
1616
use Illuminate\Testing\Concerns\TestDatabases;
17+
use RonasIT\Support\Contracts\DBTypeResolverContract;
1718
use RonasIT\Support\Contracts\VersionEnumContract as Version;
1819
use RonasIT\Support\Exceptions\BindingVersionEnumException;
1920
use RonasIT\Support\Exceptions\InvalidValidationRuleUsageException;
2021
use RonasIT\Support\Http\Middleware\SecurityMiddleware;
22+
use RonasIT\Support\Rules\DBTypeRangeRule;
23+
use RonasIT\Support\Support\PostgresDBTypeResolver;
2124
use RonasIT\Support\Support\UncountableWords;
2225

2326
class HelpersServiceProvider extends ServiceProvider
@@ -54,6 +57,7 @@ public function boot(): void
5457

5558
public function register(): void
5659
{
60+
$this->app->bind(DBTypeResolverContract::class, PostgresDBTypeResolver::class);
5761
}
5862

5963
protected function extendValidator(): void
@@ -106,6 +110,29 @@ protected function extendValidator(): void
106110

107111
return $existingValueCount === count($value);
108112
});
113+
114+
Validator::extend('db_type_range', function ($attribute, $value, $parameters, $validator) {
115+
$typeName = Arr::get($parameters, 0);
116+
117+
if (empty($typeName)) {
118+
throw new InvalidValidationRuleUsageException(
119+
message: "db_type_range: The type parameter is required when checking the {$attribute} field.",
120+
);
121+
}
122+
123+
$failed = false;
124+
125+
(new DBTypeRangeRule($typeName))->validate(
126+
attribute: $attribute,
127+
value: $value,
128+
fail: function (string $message) use ($validator, &$failed) {
129+
$validator->addReplacer('db_type_range', fn () => $message);
130+
$failed = true;
131+
},
132+
);
133+
134+
return !$failed;
135+
});
109136
}
110137

111138
protected function extendRouter(): void

src/Rules/DBTypeRangeRule.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
namespace RonasIT\Support\Rules;
4+
5+
use Closure;
6+
use Illuminate\Contracts\Validation\ValidationRule;
7+
use RonasIT\Support\Contracts\DBTypeResolverContract;
8+
use RonasIT\Support\Enums\DBTypeCategoryEnum;
9+
use RonasIT\Support\Exceptions\InvalidValidationRuleUsageException;
10+
11+
class DBTypeRangeRule implements ValidationRule
12+
{
13+
private const string INTEGER_PATTERN = '/^-?\d+$/';
14+
15+
protected DBTypeResolverContract $resolver;
16+
17+
public function __construct(
18+
protected string $type,
19+
) {
20+
$this->resolver = app(DBTypeResolverContract::class);
21+
}
22+
23+
public function validate(string $attribute, mixed $value, Closure $fail): void
24+
{
25+
if (is_null($value)) {
26+
return;
27+
}
28+
29+
if (!$this->resolver->hasType($this->type)) {
30+
throw new InvalidValidationRuleUsageException(
31+
message: "db_type_range: Unknown type '{$this->type}' for the {$attribute} field.",
32+
);
33+
}
34+
35+
list($min, $max) = $this->resolver->getRange($this->type);
36+
37+
match ($this->resolver->getTypeCategory($this->type)) {
38+
DBTypeCategoryEnum::Integer => $this->validateInteger($attribute, $value, $min, $max, $fail),
39+
DBTypeCategoryEnum::Float => $this->validateFloat($attribute, $value, $min, $max, $fail),
40+
DBTypeCategoryEnum::String => $this->validateString($attribute, $value, $max, $fail),
41+
default => null,
42+
};
43+
}
44+
45+
protected function validateInteger(string $attribute, mixed $value, mixed $min, mixed $max, Closure $fail): void
46+
{
47+
if (!is_scalar($value) || !preg_match(self::INTEGER_PATTERN, (string) $value)) {
48+
$fail("The {$attribute} must be an integer.");
49+
50+
return;
51+
}
52+
53+
$tooSmall = bccomp((string) $value, (string) $min) === -1;
54+
$tooBig = bccomp((string) $value, (string) $max) === 1;
55+
56+
if ($tooSmall || $tooBig) {
57+
$fail("The {$attribute} must be between {$min} and {$max}.");
58+
}
59+
}
60+
61+
protected function validateFloat(string $attribute, mixed $value, mixed $min, mixed $max, Closure $fail): void
62+
{
63+
if (!is_numeric($value)) {
64+
$fail("The {$attribute} must be numeric.");
65+
66+
return;
67+
}
68+
69+
if ((float) $value < $min || (float) $value > $max) {
70+
$fail("The {$attribute} must be between {$min} and {$max}.");
71+
}
72+
}
73+
74+
protected function validateString(string $attribute, mixed $value, mixed $max, Closure $fail): void
75+
{
76+
if (!is_string($value)) {
77+
$fail("The {$attribute} must be a string.");
78+
79+
return;
80+
}
81+
82+
if (mb_strlen($value) > $max) {
83+
$fail("The {$attribute} length must not exceed {$max} characters.");
84+
}
85+
}
86+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace RonasIT\Support\Support;
4+
5+
use RonasIT\Support\Contracts\DBTypeResolverContract;
6+
use RonasIT\Support\Enums\DBTypeCategoryEnum;
7+
8+
final class PostgresDBTypeResolver implements DBTypeResolverContract
9+
{
10+
public const string SMALLINT = 'smallint';
11+
public const string INTEGER = 'integer';
12+
public const string BIGINT = 'bigint';
13+
public const string SMALLSERIAL = 'smallserial';
14+
public const string SERIAL = 'serial';
15+
public const string BIGSERIAL = 'bigserial';
16+
public const string REAL = 'real';
17+
public const string DOUBLE = 'double';
18+
public const string VARCHAR = 'varchar';
19+
20+
private const array RANGES = [
21+
self::SMALLINT => [-32768, 32767],
22+
self::INTEGER => [-2147483648, 2147483647],
23+
self::BIGINT => ['-9223372036854775808', '9223372036854775807'],
24+
self::SMALLSERIAL => [1, 32767],
25+
self::SERIAL => [1, 2147483647],
26+
self::BIGSERIAL => ['1', '9223372036854775807'],
27+
self::REAL => [-3.4028234663852886e+38, 3.4028234663852886e+38],
28+
self::DOUBLE => [-PHP_FLOAT_MAX, PHP_FLOAT_MAX],
29+
self::VARCHAR => [0, 255],
30+
];
31+
32+
public function getRange(string $type): array
33+
{
34+
return self::RANGES[$type];
35+
}
36+
37+
public function hasType(string $type): bool
38+
{
39+
return array_key_exists($type, self::RANGES);
40+
}
41+
42+
public function getTypeCategory(string $type): ?DBTypeCategoryEnum
43+
{
44+
return match ($type) {
45+
self::SMALLINT, self::INTEGER, self::SMALLSERIAL, self::SERIAL, self::BIGINT, self::BIGSERIAL => DBTypeCategoryEnum::Integer,
46+
self::REAL, self::DOUBLE => DBTypeCategoryEnum::Float,
47+
self::VARCHAR => DBTypeCategoryEnum::String,
48+
default => null,
49+
};
50+
}
51+
}

0 commit comments

Comments
 (0)