Skip to content

Commit 469bf91

Browse files
committed
feat: Add ParentValidation, ScopedExistsRule, and ScopedUniqueRule for enhanced validation
1 parent 40221e4 commit 469bf91

File tree

5 files changed

+173
-3
lines changed

5 files changed

+173
-3
lines changed

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
"require": {
1818
"php": "^8.3|^8.4|^8.5",
1919
"illuminate/support": "~12",
20-
"laravel/framework": "^12.40",
20+
"laravel/framework": "^12.49",
2121
"laravel/tinker": "^2.10"
2222
},
2323
"require-dev": {
2424
"rector/rector": "^2.2",
25-
"larastan/larastan": "^3.8",
25+
"larastan/larastan": "^3.9",
2626
"laravel/pint": "^1.26"
2727
},
2828
"repositories": [

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"private": true,
33
"dependencies": {
4-
"cantil": "^1.1.2",
4+
"cantil": "^1.1.6",
55
"normalize.css": "^8.0.1"
66
}
77
}

src/app/Rules/ParentValidation.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace GemaDigital\Rules;
4+
5+
use Closure;
6+
use Illuminate\Contracts\Validation\ValidationRule;
7+
use Illuminate\Database\Eloquent\Model;
8+
use Illuminate\Support\Facades\DB;
9+
10+
class ParentValidation implements ValidationRule
11+
{
12+
public function __construct(
13+
protected ?Model $model = null,
14+
) {}
15+
16+
/**
17+
* Run the validation rule.
18+
*/
19+
public function validate(string $attribute, mixed $value, Closure $fail): void
20+
{
21+
if ($this->model === null) {
22+
return;
23+
}
24+
25+
if ($this->isDescendantOf($value)) {
26+
$fail(__('validation.parent_recursive', [
27+
'model' => class_basename($this->model),
28+
]));
29+
}
30+
}
31+
32+
/**
33+
* Check if this model is descendant of the given parent model or a recursive parent.
34+
*/
35+
public function isDescendantOf(mixed $value): bool
36+
{
37+
if ($this->model->getKey() == $value) {
38+
return true;
39+
}
40+
41+
$tableName = $this->model->getTable();
42+
$result = DB::select(
43+
"WITH RECURSIVE descendants AS (
44+
SELECT id, parent_id FROM $tableName WHERE parent_id = ?
45+
UNION ALL
46+
SELECT b.id, b.parent_id FROM $tableName b INNER JOIN descendants d ON b.parent_id = d.id
47+
)
48+
SELECT id FROM descendants WHERE id = ?;", [$this->model->getKey(), (int) $value]
49+
);
50+
51+
return count($result) > 0;
52+
}
53+
}

src/app/Rules/ScopedExistsRule.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace GemaDigital\Rules;
4+
5+
use Closure;
6+
use Illuminate\Contracts\Validation\ValidationRule;
7+
8+
final class ScopedExistsRule implements ValidationRule
9+
{
10+
public function __construct(
11+
protected string $model,
12+
protected ?string $field = null,
13+
) {}
14+
15+
/**
16+
* Create a new instance from a var_export export.
17+
*
18+
* @param array<string, mixed> $state
19+
*/
20+
public static function __set_state(array $state): static
21+
{
22+
return new self(
23+
model: $state['model'],
24+
field: $state['field'] ?? null,
25+
);
26+
}
27+
28+
/**
29+
* Run the validation rule.
30+
*
31+
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
32+
*/
33+
public function validate(string $attribute, mixed $value, Closure $fail): void
34+
{
35+
if ($value === null) {
36+
return;
37+
}
38+
39+
$model = app($this->model);
40+
$field = $this->field ?? $model->getKeyName();
41+
42+
if (is_array($value) && $model->whereIn($field, $value)->count() !== count($value)) {
43+
$fail(__('validation.exists'));
44+
45+
return;
46+
}
47+
48+
if ($model->where($field, $value)->count() === 0) {
49+
$fail(__('validation.exists'));
50+
51+
return;
52+
}
53+
}
54+
}

src/app/Rules/ScopedUniqueRule.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace GemaDigital\Rules;
4+
5+
use Closure;
6+
use Illuminate\Contracts\Validation\ValidationRule;
7+
use Illuminate\Database\Query\Builder;
8+
9+
final class ScopedUniqueRule implements ValidationRule
10+
{
11+
public function __construct(
12+
protected string $model,
13+
protected ?string $field = null,
14+
protected ?int $ignoreId = null,
15+
) {}
16+
17+
/**
18+
* Create a new instance from a var_export export.
19+
*
20+
* @param array<string, mixed> $state
21+
*/
22+
public static function __set_state(array $state): static
23+
{
24+
return new self(
25+
model: $state['model'],
26+
field: $state['field'] ?? null,
27+
ignoreId: $state['ignoreId'] ?? null,
28+
);
29+
}
30+
31+
/**
32+
* Run the validation rule.
33+
*
34+
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
35+
*/
36+
public function validate(string $attribute, mixed $value, Closure $fail): void
37+
{
38+
if ($value === null) {
39+
return;
40+
}
41+
42+
$model = app($this->model);
43+
$field = $this->field ?? $model->getKeyName();
44+
45+
/** @var Builder */
46+
$query = is_array($value)
47+
? $model->whereIn($field, $value)
48+
: $model->where($field, $value);
49+
50+
if ($this->ignoreId) {
51+
is_array($value)
52+
? $model->whereNotIn($model->getKeyName(), $this->ignoreId)
53+
: $query->whereNot($model->getKeyName(), $this->ignoreId);
54+
55+
}
56+
57+
if ($query->count() > 0) {
58+
$fail(__('validation.unique'));
59+
60+
return;
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)