Skip to content

Commit 58f15f9

Browse files
authored
feat(database): model validation before update, create, and save (#1131)
1 parent 2181ec8 commit 58f15f9

File tree

6 files changed

+140
-6
lines changed

6 files changed

+140
-6
lines changed

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
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\SkipValidation;
11+
use Tempest\Validation\Validator;
912

1013
use function Tempest\get;
1114

@@ -62,6 +65,10 @@ public function getPropertyValues(): array
6265
$values = [];
6366

6467
foreach ($this->modelClass->getProperties() as $property) {
68+
if (! $property->isInitialized($this->model)) {
69+
continue;
70+
}
71+
6572
if ($property->getIterableType()?->isRelation()) {
6673
continue;
6774
}
@@ -73,4 +80,35 @@ public function getPropertyValues(): array
7380

7481
return $values;
7582
}
83+
84+
public function validate(mixed ...$data): void
85+
{
86+
if (! $this->isObjectModel()) {
87+
return;
88+
}
89+
90+
$validator = new Validator();
91+
$failingRules = [];
92+
93+
foreach ($data as $key => $value) {
94+
$property = $this->modelClass->getProperty($key);
95+
96+
if ($property->hasAttribute(SkipValidation::class)) {
97+
continue;
98+
}
99+
100+
$failingRulesForProperty = $validator->validateValueForProperty(
101+
$property,
102+
$value,
103+
);
104+
105+
if ($failingRulesForProperty !== []) {
106+
$failingRules[$key] = $failingRulesForProperty;
107+
}
108+
}
109+
110+
if ($failingRules !== []) {
111+
throw new ValidationException($this->modelClass->getName(), $failingRules);
112+
}
113+
}
76114
}

src/Tempest/Database/src/IsDatabaseModel.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,19 @@ public static function find(mixed ...$conditions): SelectQueryBuilder
6262

6363
public static function create(mixed ...$params): self
6464
{
65-
return self::new(...$params)->save();
65+
model(self::class)->validate(...$params);
66+
67+
$model = self::new(...$params);
68+
69+
$model->id = query(self::class)
70+
->insert($model)
71+
->build()
72+
->execute();
73+
74+
return $model;
6675
}
6776

68-
public static function updateOrNew(array $find, array $update): self
77+
public static function findOrNew(array $find, array $update): self
6978
{
7079
$existing = self::select()->bind(...$find);
7180

@@ -84,7 +93,13 @@ public static function updateOrNew(array $find, array $update): self
8493

8594
public static function updateOrCreate(array $find, array $update): self
8695
{
87-
return self::updateOrNew($find, $update)->save();
96+
$model = self::findOrNew($find, $update);
97+
98+
if (! isset($model->id)) {
99+
return self::create(...$update);
100+
}
101+
102+
return $model->save();
88103
}
89104

90105
public function __get(string $name): mixed
@@ -123,6 +138,10 @@ public function load(string ...$relations): self
123138

124139
public function save(): self
125140
{
141+
$model = model($this);
142+
143+
$model->validate(...model($this)->getPropertyValues());
144+
126145
if (! isset($this->id)) {
127146
$query = query($this::class)->insert($this);
128147

@@ -138,6 +157,8 @@ public function save(): self
138157

139158
public function update(mixed ...$params): self
140159
{
160+
model(self::class)->validate(...$params);
161+
141162
foreach ($params as $key => $value) {
142163
$this->{$key} = $value;
143164
}

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
}

tests/Integration/ORM/IsDatabaseModelTest.php

Lines changed: 53 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,10 +43,12 @@
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;
4850

51+
use function Tempest\Database\model;
4952
use function Tempest\map;
5053

5154
/**
@@ -540,4 +543,54 @@ public function test_table_name_overrides(): void
540543
$this->assertEquals('custom_attribute_table_name', new ModelDefinition(AttributeTableNameModel::class)->getTableDefinition()->name);
541544
$this->assertEquals('custom_static_method_table_name', new ModelDefinition(StaticMethodTableNameModel::class)->getTableDefinition()->name);
542545
}
546+
547+
public function test_validation_on_create(): void
548+
{
549+
$this->expectException(ValidationException::class);
550+
551+
ModelWithValidation::create(
552+
index: -1,
553+
);
554+
}
555+
556+
public function test_validation_on_update(): void
557+
{
558+
$model = ModelWithValidation::new(
559+
id: new Id(1),
560+
index: 1,
561+
);
562+
563+
$this->expectException(ValidationException::class);
564+
565+
$model->update(
566+
index: -1,
567+
);
568+
}
569+
570+
public function test_validation_on_new(): void
571+
{
572+
$model = ModelWithValidation::new(
573+
index: 1,
574+
);
575+
576+
$model->index = -1;
577+
578+
$this->expectException(ValidationException::class);
579+
580+
$model->save();
581+
}
582+
583+
public function test_skipped_validation(): void
584+
{
585+
try {
586+
model(ModelWithValidation::class)->validate(
587+
index: -1,
588+
skip: -1,
589+
);
590+
} catch (ValidationException $validationException) {
591+
$this->assertStringContainsString('index', $validationException->getMessage());
592+
$this->assertStringContainsString(ModelWithValidation::class, $validationException->getMessage());
593+
$this->assertStringNotContainsString('skip', $validationException->getMessage());
594+
}
595+
}
543596
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Tests\Tempest\Integration\ORM\Models;
4+
5+
use Tempest\Database\IsDatabaseModel;
6+
use Tempest\Validation\Rules\Between;
7+
use Tempest\Validation\SkipValidation;
8+
9+
final class ModelWithValidation
10+
{
11+
use IsDatabaseModel;
12+
13+
#[Between(min: 1, max: 10)]
14+
public int $index;
15+
16+
#[SkipValidation]
17+
public int $skip;
18+
}

0 commit comments

Comments
 (0)