Skip to content

Add make and uuid functions #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 53 additions & 9 deletions src/Rules/ExistsEloquent.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

class ExistsEloquent implements ValidationRule
{
Expand Down Expand Up @@ -53,6 +54,11 @@ class ExistsEloquent implements ValidationRule
*/
private bool $includeSoftDeleted = false;

/**
* @var bool Whether the key field is of type UUID
*/
private bool $isFieldUuid = false;

/**
* Create a new rule instance.
*
Expand All @@ -67,6 +73,18 @@ public function __construct(string $model, ?string $key = null, ?Closure $builde
$this->setBuilderClosure($builderClosure);
}

/**
* Create a new rule instance.
*
* @param class-string<Model> $model Class name of model
* @param string|null $key Relevant key in the model
* @param Closure|null $builderClosure Closure that can extend the eloquent builder
*/
public static function make(string $model, ?string $key = null, ?Closure $builderClosure = null): self
{
return new self($model, $key, $builderClosure);
}

/**
* Set a custom validation message.
*
Expand All @@ -93,6 +111,20 @@ public function withCustomTranslation(string $translationKey): self
return $this;
}

/**
* The field has the data type UUID.
* If the field is not a UUID, the validation will fail, before the query is executed.
* 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.
*
* @return $this
*/
public function uuid(): self
{
$this->isFieldUuid = true;

return $this;
}

/**
* Determine if the validation rule passes.
*
Expand All @@ -104,6 +136,12 @@ public function withCustomTranslation(string $translationKey): self
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if ($this->isFieldUuid) {
if (!is_string($value) || !Str::isUuid($value)) {
$this->fail($attribute, $value, $fail);
return;
}
}
/** @var Model|Builder $builder */
$builder = new $this->model();
$modelKeyName = $builder->getKeyName();
Expand All @@ -122,15 +160,21 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
}

if ($builder->doesntExist()) {
if ($this->customMessage !== null) {
$fail($this->customMessage);
} else {
$fail($this->customMessageTranslationKey ?? 'modelValidationRules::validation.exists_model')->translate([
'attribute' => $attribute,
'model' => strtolower(class_basename($this->model)),
'value' => $value,
]);
}
$this->fail($attribute, $value, $fail);
return;
}
}

private function fail(string $attribute, mixed $value, Closure $fail): void
{
if ($this->customMessage !== null) {
$fail($this->customMessage);
} else {
$fail($this->customMessageTranslationKey ?? 'modelValidationRules::validation.exists_model')->translate([
'attribute' => $attribute,
'model' => strtolower(class_basename($this->model)),
'value' => $value,
]);
}
}

Expand Down
72 changes: 54 additions & 18 deletions src/Rules/UniqueEloquent.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

class UniqueEloquent implements ValidationRule
{
Expand Down Expand Up @@ -63,12 +64,17 @@ class UniqueEloquent implements ValidationRule
*/
private bool $includeSoftDeleted = false;

/**
* @var bool Whether the ID is a UUID
*/
private bool $isFieldUuid = false;

/**
* UniqueEloquent constructor.
*
* @param class-string<Model> $model Class name of model.
* @param string|null $key Relevant key in the model.
* @param Closure|null $builderClosure Closure that can extend the eloquent builder
* @param class-string<Model> $model Class name of model.
* @param string|null $key Relevant key in the model.
* @param Closure|null $builderClosure Closure that can extend the eloquent builder
*/
public function __construct(string $model, ?string $key = null, ?Closure $builderClosure = null)
{
Expand All @@ -77,17 +83,32 @@ public function __construct(string $model, ?string $key = null, ?Closure $builde
$this->setBuilderClosure($builderClosure);
}

/**
* @param class-string<Model> $model Class name of model.
* @param string|null $key Relevant key in the model.
* @param Closure|null $builderClosure Closure that can extend the eloquent builder
*/
public static function make(string $model, ?string $key = null, ?Closure $builderClosure = null): self
{
return new self($model, $key, $builderClosure);
}

/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @param Closure $fail
* @param string $attribute
* @param mixed $value
* @param Closure $fail
*
* @return void
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if ($this->isFieldUuid) {
if (!is_string($value) || !Str::isUuid($value)) {
return;
}
}
/** @var Model|Builder $builder */
$builder = new $this->model();
$modelKeyName = $builder->getKeyName();
Expand All @@ -112,19 +133,20 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
if ($this->customMessage !== null) {
$fail($this->customMessage);
} else {
$fail($this->customMessageTranslationKey ?? 'modelValidationRules::validation.unique_model')->translate([
'attribute' => $attribute,
'model' => strtolower(class_basename($this->model)),
'value' => $value,
]);
$fail($this->customMessageTranslationKey ?? 'modelValidationRules::validation.unique_model')
->translate([
'attribute' => $attribute,
'model' => strtolower(class_basename($this->model)),
'value' => $value,
]);
}
}
}

/**
* Set a custom validation message.
*
* @param string $message
* @param string $message
* @return $this
*/
public function withMessage(string $message): self
Expand All @@ -137,7 +159,7 @@ public function withMessage(string $message): self
/**
* Set a translated custom validation message.
*
* @param string $translationKey
* @param string $translationKey
* @return $this
*/
public function withCustomTranslation(string $translationKey): self
Expand All @@ -150,15 +172,15 @@ public function withCustomTranslation(string $translationKey): self
/**
* Set a closure that can extend the eloquent builder.
*
* @param Closure|null $builderClosure
* @param Closure|null $builderClosure
*/
public function setBuilderClosure(?Closure $builderClosure): void
{
$this->builderClosure = $builderClosure;
}

/**
* @param Closure $builderClosure
* @param Closure $builderClosure
* @return $this
*/
public function query(Closure $builderClosure): self
Expand All @@ -169,8 +191,8 @@ public function query(Closure $builderClosure): self
}

/**
* @param mixed $id
* @param string|null $column
* @param mixed $id
* @param string|null $column
*/
public function setIgnore(mixed $id, ?string $column = null): void
{
Expand All @@ -180,7 +202,7 @@ public function setIgnore(mixed $id, ?string $column = null): void

/**
* @param mixed $id
* @param string|null $column
* @param string|null $column
* @return UniqueEloquent
*/
public function ignore(mixed $id, ?string $column = null): self
Expand All @@ -201,6 +223,20 @@ public function setIncludeSoftDeleted(bool $includeSoftDeleted): void
$this->includeSoftDeleted = $includeSoftDeleted;
}

/**
* The field has the data type UUID.
* If a value is not a UUID, the validation will be skipped.
* 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.
*
* @return $this
*/
public function uuid(): self
{
$this->isFieldUuid = true;

return $this;
}

/**
* Activate including soft deleted models in the query.
*
Expand Down
62 changes: 62 additions & 0 deletions tests/Feature/ExistsEloquentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
Expand All @@ -30,12 +31,15 @@ public function testValidationFailsIfEntryDoesNotExistInDatabase(): void
], [
'id' => [new ExistsEloquent(User::class)]
]);
$this->db->enableQueryLog();

// Act
$isValid = $validator->passes();
$messages = $validator->messages()->toArray();

// Assert
$queryLog = $this->db->getQueryLog();
$this->assertCount(1, $queryLog);
$this->assertFalse($isValid);
$this->assertEquals('The resource does not exist.', $messages['id'][0]);
}
Expand Down Expand Up @@ -405,4 +409,62 @@ public function testValidationMessageIsLaravelTranslationIfCustomTranslationIsSe
$this->assertFalse($isValid);
$this->assertEquals('A user with the id "1" does not exist. / Test', $messages['id'][0]);
}

public function testFunctionMakeIsIdenticalToConstructor(): void
{
// Arrange
$message = 'Test';
$closure = function (Builder $builder) {
return $builder->where('user_id', 6);
};

// Act
$rule1 = ExistsEloquent::make(User::class, 'other_id', $closure)->withMessage($message);
$rule2 = (new ExistsEloquent(User::class, 'other_id', $closure))->withMessage($message);

// Assert
$this->assertEquals($rule1, $rule2);
}

public function testUuidOptionMakesRuleFailIfValueIsNotUuidBeforeQueryingTheDatabase(): void
{
// Arrange
$validator = Validator::make([
'id' => 'not-a-uuid',
], [
'id' => [(new ExistsEloquent(User::class))->uuid()]
]);
$this->db->enableQueryLog();

// Act
$isValid = $validator->passes();
$messages = $validator->messages()->toArray();

// Assert
$queryLog = $this->db->getQueryLog();
$this->assertCount(0, $queryLog);
$this->assertFalse($isValid);
$this->assertEquals('The resource does not exist.', $messages['id'][0]);
}

public function testUuidOptionMakesRuleFailIfValueIsNotStringBeforeQueryingTheDatabase(): void
{
// Arrange
$validator = Validator::make([
'id' => 1,
], [
'id' => [(new ExistsEloquent(User::class))->uuid()]
]);
$this->db->enableQueryLog();

// Act
$isValid = $validator->passes();
$messages = $validator->messages()->toArray();

// Assert
$queryLog = $this->db->getQueryLog();
$this->assertCount(0, $queryLog);
$this->assertFalse($isValid);
$this->assertEquals('The resource does not exist.', $messages['id'][0]);
}
}
Loading