Skip to content

Commit 134d8a0

Browse files
committed
Model validation
1 parent 2181ec8 commit 134d8a0

File tree

7 files changed

+91
-4
lines changed

7 files changed

+91
-4
lines changed

src/Tempest/Database/src/Builder/ModelInspector.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use Tempest\Database\Config\DatabaseConfig;
77
use Tempest\Database\Table;
88
use Tempest\Reflection\ClassReflector;
9+
use Tempest\Validation\Exceptions\ValidationException;
10+
use Tempest\Validation\Validator;
911

1012
use function Tempest\get;
1113

@@ -73,4 +75,25 @@ public function getPropertyValues(): array
7375

7476
return $values;
7577
}
78+
79+
public function validate(mixed ...$data): void
80+
{
81+
if ($this->modelClass === null) {
82+
return;
83+
}
84+
85+
$validator = new Validator();
86+
$failingRules = [];
87+
88+
foreach ($data as $key => $value) {
89+
$failingRules = [...$failingRules, ...$validator->validateValueForProperty(
90+
$this->modelClass->getProperty($key),
91+
$value,
92+
)];
93+
}
94+
95+
if ($failingRules !== []) {
96+
throw new ValidationException(self::class, $failingRules);
97+
}
98+
}
7699
}

src/Tempest/Database/src/IsDatabaseModel.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public static function find(mixed ...$conditions): SelectQueryBuilder
6262

6363
public static function create(mixed ...$params): self
6464
{
65+
model(self::class)->validate(...$params);
66+
6567
return self::new(...$params)->save();
6668
}
6769

@@ -138,6 +140,8 @@ public function save(): self
138140

139141
public function update(mixed ...$params): self
140142
{
143+
model(self::class)->validate(...$params);
144+
141145
foreach ($params as $key => $value) {
142146
$this->{$key} = $value;
143147
}

src/Tempest/Router/src/GenericRouter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ private function processRequest(Request|PsrRequest $request): Response
8080
} catch (NotFoundException) {
8181
return new NotFound();
8282
} catch (ValidationException $validationException) {
83-
return new Invalid($validationException->object, $validationException->failingRules);
83+
return new Invalid($validationException->subject, $validationException->failingRules);
8484
}
8585
} else {
8686
$request = $this->resolveRequest($request, $matchedRoute);

src/Tempest/Validation/src/Exceptions/ValidationException.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
final class ValidationException extends Exception
1313
{
1414
public function __construct(
15-
public readonly object $object,
15+
public readonly object|string $subject,
1616
public readonly array $failingRules,
1717
) {
1818
$messages = [];
@@ -24,6 +24,10 @@ public function __construct(
2424
}
2525
}
2626

27-
parent::__construct($object::class . PHP_EOL . json_encode($messages, JSON_PRETTY_PRINT));
27+
if (is_object($subject)) {
28+
$subject = $subject::class;
29+
}
30+
31+
parent::__construct($subject . PHP_EOL . json_encode($messages, JSON_PRETTY_PRINT));
2832
}
2933
}

src/Tempest/Validation/src/Validator.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
use Closure;
88
use Tempest\Reflection\ClassReflector;
99
use Tempest\Reflection\PropertyReflector;
10+
use Tempest\Support\Arr\ImmutableArray;
1011
use Tempest\Validation\Exceptions\ValidationException;
1112
use Tempest\Validation\Rules\IsBoolean;
1213
use Tempest\Validation\Rules\IsEnum;
1314
use Tempest\Validation\Rules\IsFloat;
1415
use Tempest\Validation\Rules\IsInteger;
1516
use Tempest\Validation\Rules\IsString;
1617
use Tempest\Validation\Rules\NotNull;
18+
use UnitEnum;
1719

1820
use function Tempest\Support\arr;
1921

@@ -49,7 +51,7 @@ public function validateValuesForClass(ClassReflector|string $class, ?array $val
4951

5052
$failingRules = [];
5153

52-
$values = arr($values)->undot();
54+
$values = $this->resolveValues($values);
5355

5456
foreach ($class->getPublicProperties() as $property) {
5557
if ($property->hasAttribute(SkipValidation::class)) {
@@ -163,4 +165,19 @@ public function message(): string
163165
}
164166
};
165167
}
168+
169+
private function resolveValues(mixed $values): mixed
170+
{
171+
if (is_object($values) && ! ($values instanceof UnitEnum)) {
172+
return new ImmutableArray((array) $values);
173+
}
174+
175+
if (is_array($values)) {
176+
return arr($values)
177+
->undot()
178+
->map(fn (mixed $value) => $this->resolveValues($value));
179+
}
180+
181+
return $values;
182+
}
166183
}

tests/Integration/ORM/IsDatabaseModelTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Tempest\Database\Migrations\CreateMigrationsTable;
1414
use Tempest\Mapper\CasterFactory;
1515
use Tempest\Mapper\SerializerFactory;
16+
use Tempest\Validation\Exceptions\ValidationException;
1617
use Tests\Tempest\Fixtures\Migrations\CreateAuthorTable;
1718
use Tests\Tempest\Fixtures\Migrations\CreateBookTable;
1819
use Tests\Tempest\Fixtures\Models\A;
@@ -42,6 +43,7 @@
4243
use Tests\Tempest\Integration\ORM\Models\CasterEnum;
4344
use Tests\Tempest\Integration\ORM\Models\CasterModel;
4445
use Tests\Tempest\Integration\ORM\Models\ChildModel;
46+
use Tests\Tempest\Integration\ORM\Models\ModelWithValidation;
4547
use Tests\Tempest\Integration\ORM\Models\ParentModel;
4648
use Tests\Tempest\Integration\ORM\Models\StaticMethodTableNameModel;
4749
use Tests\Tempest\Integration\ORM\Models\ThroughModel;
@@ -540,4 +542,27 @@ public function test_table_name_overrides(): void
540542
$this->assertEquals('custom_attribute_table_name', new ModelDefinition(AttributeTableNameModel::class)->getTableDefinition()->name);
541543
$this->assertEquals('custom_static_method_table_name', new ModelDefinition(StaticMethodTableNameModel::class)->getTableDefinition()->name);
542544
}
545+
546+
public function test_validation_on_create(): void
547+
{
548+
$this->expectException(ValidationException::class);
549+
550+
ModelWithValidation::create(
551+
index: -1,
552+
);
553+
}
554+
555+
public function test_validation_on_update(): void
556+
{
557+
$model = ModelWithValidation::new(
558+
id: new Id(1),
559+
index: 1,
560+
);
561+
562+
$this->expectException(ValidationException::class);
563+
564+
$model->update(
565+
index: -1,
566+
);
567+
}
543568
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Tests\Tempest\Integration\ORM\Models;
4+
5+
use Tempest\Database\IsDatabaseModel;
6+
use Tempest\Validation\Rules\Between;
7+
8+
final class ModelWithValidation
9+
{
10+
use IsDatabaseModel;
11+
12+
#[Between(min: 1, max: 10)]
13+
public int $index;
14+
}

0 commit comments

Comments
 (0)