Skip to content

Commit e7c27b9

Browse files
committed
Add make and uuid functions
1 parent 6cecadd commit e7c27b9

File tree

5 files changed

+231
-27
lines changed

5 files changed

+231
-27
lines changed

src/Rules/ExistsEloquent.php

+53-9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Illuminate\Contracts\Validation\ValidationRule;
99
use Illuminate\Database\Eloquent\Builder;
1010
use Illuminate\Database\Eloquent\Model;
11+
use Illuminate\Support\Str;
1112

1213
class ExistsEloquent implements ValidationRule
1314
{
@@ -53,6 +54,11 @@ class ExistsEloquent implements ValidationRule
5354
*/
5455
private bool $includeSoftDeleted = false;
5556

57+
/**
58+
* @var bool Whether the key field is of type UUID
59+
*/
60+
private bool $isFieldUuid = false;
61+
5662
/**
5763
* Create a new rule instance.
5864
*
@@ -67,6 +73,18 @@ public function __construct(string $model, ?string $key = null, ?Closure $builde
6773
$this->setBuilderClosure($builderClosure);
6874
}
6975

76+
/**
77+
* Create a new rule instance.
78+
*
79+
* @param class-string<Model> $model Class name of model
80+
* @param string|null $key Relevant key in the model
81+
* @param Closure|null $builderClosure Closure that can extend the eloquent builder
82+
*/
83+
public static function make(string $model, ?string $key = null, ?Closure $builderClosure = null): self
84+
{
85+
return new self($model, $key, $builderClosure);
86+
}
87+
7088
/**
7189
* Set a custom validation message.
7290
*
@@ -93,6 +111,20 @@ public function withCustomTranslation(string $translationKey): self
93111
return $this;
94112
}
95113

114+
/**
115+
* The field has the data type UUID.
116+
* If the field is not a UUID, the validation will fail, before the query is executed.
117+
* This is useful for example for Postgres databases where queries fail if a field with UUID data type is queried with a non-UUID value.
118+
*
119+
* @return $this
120+
*/
121+
public function uuid(): self
122+
{
123+
$this->isFieldUuid = true;
124+
125+
return $this;
126+
}
127+
96128
/**
97129
* Determine if the validation rule passes.
98130
*
@@ -104,6 +136,12 @@ public function withCustomTranslation(string $translationKey): self
104136
*/
105137
public function validate(string $attribute, mixed $value, Closure $fail): void
106138
{
139+
if ($this->isFieldUuid) {
140+
if (!is_string($value) || !Str::isUuid($value)) {
141+
$this->fail($attribute, $value, $fail);
142+
return;
143+
}
144+
}
107145
/** @var Model|Builder $builder */
108146
$builder = new $this->model();
109147
$modelKeyName = $builder->getKeyName();
@@ -122,15 +160,21 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
122160
}
123161

124162
if ($builder->doesntExist()) {
125-
if ($this->customMessage !== null) {
126-
$fail($this->customMessage);
127-
} else {
128-
$fail($this->customMessageTranslationKey ?? 'modelValidationRules::validation.exists_model')->translate([
129-
'attribute' => $attribute,
130-
'model' => strtolower(class_basename($this->model)),
131-
'value' => $value,
132-
]);
133-
}
163+
$this->fail($attribute, $value, $fail);
164+
return;
165+
}
166+
}
167+
168+
private function fail(string $attribute, mixed $value, Closure $fail): void
169+
{
170+
if ($this->customMessage !== null) {
171+
$fail($this->customMessage);
172+
} else {
173+
$fail($this->customMessageTranslationKey ?? 'modelValidationRules::validation.exists_model')->translate([
174+
'attribute' => $attribute,
175+
'model' => strtolower(class_basename($this->model)),
176+
'value' => $value,
177+
]);
134178
}
135179
}
136180

src/Rules/UniqueEloquent.php

+54-18
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Illuminate\Contracts\Validation\ValidationRule;
99
use Illuminate\Database\Eloquent\Builder;
1010
use Illuminate\Database\Eloquent\Model;
11+
use Illuminate\Support\Str;
1112

1213
class UniqueEloquent implements ValidationRule
1314
{
@@ -63,12 +64,17 @@ class UniqueEloquent implements ValidationRule
6364
*/
6465
private bool $includeSoftDeleted = false;
6566

67+
/**
68+
* @var bool Whether the ID is a UUID
69+
*/
70+
private bool $isFieldUuid = false;
71+
6672
/**
6773
* UniqueEloquent constructor.
6874
*
69-
* @param class-string<Model> $model Class name of model.
70-
* @param string|null $key Relevant key in the model.
71-
* @param Closure|null $builderClosure Closure that can extend the eloquent builder
75+
* @param class-string<Model> $model Class name of model.
76+
* @param string|null $key Relevant key in the model.
77+
* @param Closure|null $builderClosure Closure that can extend the eloquent builder
7278
*/
7379
public function __construct(string $model, ?string $key = null, ?Closure $builderClosure = null)
7480
{
@@ -77,17 +83,32 @@ public function __construct(string $model, ?string $key = null, ?Closure $builde
7783
$this->setBuilderClosure($builderClosure);
7884
}
7985

86+
/**
87+
* @param class-string<Model> $model Class name of model.
88+
* @param string|null $key Relevant key in the model.
89+
* @param Closure|null $builderClosure Closure that can extend the eloquent builder
90+
*/
91+
public static function make(string $model, ?string $key = null, ?Closure $builderClosure = null): self
92+
{
93+
return new self($model, $key, $builderClosure);
94+
}
95+
8096
/**
8197
* Determine if the validation rule passes.
8298
*
83-
* @param string $attribute
84-
* @param mixed $value
85-
* @param Closure $fail
99+
* @param string $attribute
100+
* @param mixed $value
101+
* @param Closure $fail
86102
*
87103
* @return void
88104
*/
89105
public function validate(string $attribute, mixed $value, Closure $fail): void
90106
{
107+
if ($this->isFieldUuid) {
108+
if (!is_string($value) || !Str::isUuid($value)) {
109+
return;
110+
}
111+
}
91112
/** @var Model|Builder $builder */
92113
$builder = new $this->model();
93114
$modelKeyName = $builder->getKeyName();
@@ -112,19 +133,20 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
112133
if ($this->customMessage !== null) {
113134
$fail($this->customMessage);
114135
} else {
115-
$fail($this->customMessageTranslationKey ?? 'modelValidationRules::validation.unique_model')->translate([
116-
'attribute' => $attribute,
117-
'model' => strtolower(class_basename($this->model)),
118-
'value' => $value,
119-
]);
136+
$fail($this->customMessageTranslationKey ?? 'modelValidationRules::validation.unique_model')
137+
->translate([
138+
'attribute' => $attribute,
139+
'model' => strtolower(class_basename($this->model)),
140+
'value' => $value,
141+
]);
120142
}
121143
}
122144
}
123145

124146
/**
125147
* Set a custom validation message.
126148
*
127-
* @param string $message
149+
* @param string $message
128150
* @return $this
129151
*/
130152
public function withMessage(string $message): self
@@ -137,7 +159,7 @@ public function withMessage(string $message): self
137159
/**
138160
* Set a translated custom validation message.
139161
*
140-
* @param string $translationKey
162+
* @param string $translationKey
141163
* @return $this
142164
*/
143165
public function withCustomTranslation(string $translationKey): self
@@ -150,15 +172,15 @@ public function withCustomTranslation(string $translationKey): self
150172
/**
151173
* Set a closure that can extend the eloquent builder.
152174
*
153-
* @param Closure|null $builderClosure
175+
* @param Closure|null $builderClosure
154176
*/
155177
public function setBuilderClosure(?Closure $builderClosure): void
156178
{
157179
$this->builderClosure = $builderClosure;
158180
}
159181

160182
/**
161-
* @param Closure $builderClosure
183+
* @param Closure $builderClosure
162184
* @return $this
163185
*/
164186
public function query(Closure $builderClosure): self
@@ -169,8 +191,8 @@ public function query(Closure $builderClosure): self
169191
}
170192

171193
/**
172-
* @param mixed $id
173-
* @param string|null $column
194+
* @param mixed $id
195+
* @param string|null $column
174196
*/
175197
public function setIgnore(mixed $id, ?string $column = null): void
176198
{
@@ -180,7 +202,7 @@ public function setIgnore(mixed $id, ?string $column = null): void
180202

181203
/**
182204
* @param mixed $id
183-
* @param string|null $column
205+
* @param string|null $column
184206
* @return UniqueEloquent
185207
*/
186208
public function ignore(mixed $id, ?string $column = null): self
@@ -201,6 +223,20 @@ public function setIncludeSoftDeleted(bool $includeSoftDeleted): void
201223
$this->includeSoftDeleted = $includeSoftDeleted;
202224
}
203225

226+
/**
227+
* The field has the data type UUID.
228+
* If a value is not a UUID, the validation will be skipped.
229+
* This is useful for example for Postgres databases where queries fail if a field with UUID data type is queried with a non-UUID value.
230+
*
231+
* @return $this
232+
*/
233+
public function uuid(): self
234+
{
235+
$this->isFieldUuid = true;
236+
237+
return $this;
238+
}
239+
204240
/**
205241
* Activate including soft deleted models in the query.
206242
*

tests/Feature/ExistsEloquentTest.php

+62
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Illuminate\Database\Eloquent\Builder;
88
use Illuminate\Foundation\Testing\RefreshDatabase;
9+
use Illuminate\Support\Facades\DB;
910
use Illuminate\Support\Facades\Lang;
1011
use Illuminate\Support\Facades\Validator;
1112
use Illuminate\Support\Str;
@@ -30,12 +31,15 @@ public function testValidationFailsIfEntryDoesNotExistInDatabase(): void
3031
], [
3132
'id' => [new ExistsEloquent(User::class)]
3233
]);
34+
$this->db->enableQueryLog();
3335

3436
// Act
3537
$isValid = $validator->passes();
3638
$messages = $validator->messages()->toArray();
3739

3840
// Assert
41+
$queryLog = $this->db->getQueryLog();
42+
$this->assertCount(1, $queryLog);
3943
$this->assertFalse($isValid);
4044
$this->assertEquals('The resource does not exist.', $messages['id'][0]);
4145
}
@@ -405,4 +409,62 @@ public function testValidationMessageIsLaravelTranslationIfCustomTranslationIsSe
405409
$this->assertFalse($isValid);
406410
$this->assertEquals('A user with the id "1" does not exist. / Test', $messages['id'][0]);
407411
}
412+
413+
public function testFunctionMakeIsIdenticalToConstructor(): void
414+
{
415+
// Arrange
416+
$message = 'Test';
417+
$closure = function (Builder $builder) {
418+
return $builder->where('user_id', 6);
419+
};
420+
421+
// Act
422+
$rule1 = ExistsEloquent::make(User::class, 'other_id', $closure)->withMessage($message);
423+
$rule2 = (new ExistsEloquent(User::class, 'other_id', $closure))->withMessage($message);
424+
425+
// Assert
426+
$this->assertEquals($rule1, $rule2);
427+
}
428+
429+
public function testUuidOptionMakesRuleFailIfValueIsNotUuidBeforeQueryingTheDatabase(): void
430+
{
431+
// Arrange
432+
$validator = Validator::make([
433+
'id' => 'not-a-uuid',
434+
], [
435+
'id' => [(new ExistsEloquent(User::class))->uuid()]
436+
]);
437+
$this->db->enableQueryLog();
438+
439+
// Act
440+
$isValid = $validator->passes();
441+
$messages = $validator->messages()->toArray();
442+
443+
// Assert
444+
$queryLog = $this->db->getQueryLog();
445+
$this->assertCount(0, $queryLog);
446+
$this->assertFalse($isValid);
447+
$this->assertEquals('The resource does not exist.', $messages['id'][0]);
448+
}
449+
450+
public function testUuidOptionMakesRuleFailIfValueIsNotStringBeforeQueryingTheDatabase(): void
451+
{
452+
// Arrange
453+
$validator = Validator::make([
454+
'id' => 1,
455+
], [
456+
'id' => [(new ExistsEloquent(User::class))->uuid()]
457+
]);
458+
$this->db->enableQueryLog();
459+
460+
// Act
461+
$isValid = $validator->passes();
462+
$messages = $validator->messages()->toArray();
463+
464+
// Assert
465+
$queryLog = $this->db->getQueryLog();
466+
$this->assertCount(0, $queryLog);
467+
$this->assertFalse($isValid);
468+
$this->assertEquals('The resource does not exist.', $messages['id'][0]);
469+
}
408470
}

0 commit comments

Comments
 (0)