Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
35 changes: 35 additions & 0 deletions docs/validation/using-validation-attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,41 @@ The rules will now look like this:
]
```

## Using database constraints

When using `Exists` and `Unique` validation attributes, you can add database constraints to validate against specific conditions:

```php
class UserData extends Data
{
public function __construct(
#[Exists('users', where: new WhereConstraint('active', true))]
public int $user_id,

#[Unique('users', 'email', where: new WhereNullConstraint('deleted_at'))]
public string $email,
) {
}
}
```

You can also combine multiple constraints:

```php
class ProductData extends Data
{
public function __construct(
#[Unique('products', 'sku', where: [
new WhereConstraint('active', true),
new WhereInConstraint('type', ['physical', 'digital']),
new WhereNullConstraint('deleted_at'),
])]
public string $sku,
) {
}
}
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a list here with the possible constraints?


## Rule attribute

One special attribute is the `Rule` attribute. With it, you can write rules just like you would when creating a custom
Expand Down
25 changes: 23 additions & 2 deletions src/Attributes/Validation/Exists.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@
use Closure;
use Exception;
use Illuminate\Validation\Rules\Exists as BaseExists;
use InvalidArgumentException;
use Spatie\LaravelData\Support\Validation\Constraints\WhereConstraint;
use Spatie\LaravelData\Support\Validation\Constraints\WhereNotConstraint;
use Spatie\LaravelData\Support\Validation\Constraints\WhereNullConstraint;
use Spatie\LaravelData\Support\Validation\Constraints\WhereNotNullConstraint;
use Spatie\LaravelData\Support\Validation\Constraints\WhereInConstraint;
use Spatie\LaravelData\Support\Validation\Constraints\WhereNotInConstraint;
use Spatie\LaravelData\Support\Validation\References\ExternalReference;
use Spatie\LaravelData\Support\Validation\ValidationPath;
use Spatie\LaravelData\Support\Validation\Constraints\DatabaseConstraint;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class Exists extends ObjectValidationAttribute
Expand All @@ -18,7 +26,7 @@ public function __construct(
protected null|string|ExternalReference $connection = null,
protected bool|ExternalReference $withoutTrashed = false,
protected string|ExternalReference $deletedAtColumn = 'deleted_at',
protected ?Closure $where = null,
protected null|Closure|DatabaseConstraint|array $where = null,
protected ?BaseExists $rule = null,
) {
if ($rule === null && $table === null) {
Expand Down Expand Up @@ -48,7 +56,20 @@ public function getRule(ValidationPath $path): object|string
}

if ($this->where) {
$rule->where($this->where);
$constraints = is_array($this->where) ? $this->where : [$this->where];

foreach ($constraints as $constraint) {
match (true) {
$constraint instanceof Closure => $rule->where($constraint),
$constraint instanceof WhereConstraint => $rule->where(...$constraint->toArray()),
$constraint instanceof WhereNotConstraint => $rule->whereNot(...$constraint->toArray()),
$constraint instanceof WhereNullConstraint => $rule->whereNull(...$constraint->toArray()),
$constraint instanceof WhereNotNullConstraint => $rule->whereNotNull(...$constraint->toArray()),
$constraint instanceof WhereInConstraint => $rule->whereIn(...$constraint->toArray()),
$constraint instanceof WhereNotInConstraint => $rule->whereNotIn(...$constraint->toArray()),
default => throw new InvalidArgumentException('Each where item must be a DatabaseConstraint or Closure'),
};
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to move this to a trait since we duplicate the code?

}

return $rule;
Expand Down
25 changes: 23 additions & 2 deletions src/Attributes/Validation/Unique.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@
use Closure;
use Exception;
use Illuminate\Validation\Rules\Unique as BaseUnique;
use InvalidArgumentException;
use Spatie\LaravelData\Support\Validation\Constraints\WhereConstraint;
use Spatie\LaravelData\Support\Validation\Constraints\WhereNotConstraint;
use Spatie\LaravelData\Support\Validation\Constraints\WhereNullConstraint;
use Spatie\LaravelData\Support\Validation\Constraints\WhereNotNullConstraint;
use Spatie\LaravelData\Support\Validation\Constraints\WhereInConstraint;
use Spatie\LaravelData\Support\Validation\Constraints\WhereNotInConstraint;
use Spatie\LaravelData\Support\Validation\References\ExternalReference;
use Spatie\LaravelData\Support\Validation\ValidationPath;
use Spatie\LaravelData\Support\Validation\Constraints\DatabaseConstraint;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class Unique extends ObjectValidationAttribute
Expand All @@ -20,7 +28,7 @@ public function __construct(
protected null|string|ExternalReference $ignoreColumn = null,
protected bool|ExternalReference $withoutTrashed = false,
protected string|ExternalReference $deletedAtColumn = 'deleted_at',
protected ?Closure $where = null,
protected null|Closure|DatabaseConstraint|array $where = null,
protected ?BaseUnique $rule = null
) {
if ($table === null && $rule === null) {
Expand Down Expand Up @@ -56,7 +64,20 @@ public function getRule(ValidationPath $path): object|string
}

if ($this->where) {
$rule->where($this->where);
$constraints = is_array($this->where) ? $this->where : [$this->where];

foreach ($constraints as $constraint) {
match (true) {
$constraint instanceof Closure => $rule->where($constraint),
$constraint instanceof WhereConstraint => $rule->where(...$constraint->toArray()),
$constraint instanceof WhereNotConstraint => $rule->whereNot(...$constraint->toArray()),
$constraint instanceof WhereNullConstraint => $rule->whereNull(...$constraint->toArray()),
$constraint instanceof WhereNotNullConstraint => $rule->whereNotNull(...$constraint->toArray()),
$constraint instanceof WhereInConstraint => $rule->whereIn(...$constraint->toArray()),
$constraint instanceof WhereNotInConstraint => $rule->whereNotIn(...$constraint->toArray()),
default => throw new InvalidArgumentException('Each where item must be a DatabaseConstraint or Closure'),
};
}
}

return $rule;
Expand Down
9 changes: 9 additions & 0 deletions src/Support/Validation/Constraints/DatabaseConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Spatie\LaravelData\Support\Validation\Constraints;

use Illuminate\Contracts\Support\Arrayable;

interface DatabaseConstraint extends Arrayable
{
}
17 changes: 17 additions & 0 deletions src/Support/Validation/Constraints/WhereConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Spatie\LaravelData\Support\Validation\Constraints;

class WhereConstraint implements DatabaseConstraint
{
public function __construct(
public readonly mixed $column,
public readonly mixed $value = null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know indeed laravel has a mixed type but they provide typehints, let's use those. For value mixed is allright since it probably is really mixed. Another thing I would like to see is support for ExternalReference since we can dynamically replace the value which might be useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion! I've updated the PR to allow the use of ExternalReference. I also added types to the database constraints to the ones that I found made sense.

I took the type suggestions from https://api.laravel.com/docs/11.x/Illuminate/Validation/Rules/DatabaseRule.html

I kept the $value of the WhereConstraint and WhereNotConstraint as mixed, like you said they probably really are mixed.

The type suggestion for whereNot does not include null, but that is supported.

This would otherwise error:

new WhereNotConstraint('deleted_at', null)

Which is not a big deal, since you can use WhereNullConstraint instead, but it makes me think there are other inputs which are not in the type but are supported

) {
}

public function toArray(): array
{
return [$this->column, $this->value];
}
}
16 changes: 16 additions & 0 deletions src/Support/Validation/Constraints/WhereInConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Spatie\LaravelData\Support\Validation\Constraints;

class WhereInConstraint implements DatabaseConstraint
{
public function __construct(
public readonly mixed $column,
public readonly mixed $values,
) {}

public function toArray(): array
{
return [$this->column, $this->values];
}
}
16 changes: 16 additions & 0 deletions src/Support/Validation/Constraints/WhereNotConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Spatie\LaravelData\Support\Validation\Constraints;

class WhereNotConstraint implements DatabaseConstraint
{
public function __construct(
public readonly mixed $column,
public readonly mixed $value,
) {}

public function toArray(): array
{
return [$this->column, $this->value];
}
}
16 changes: 16 additions & 0 deletions src/Support/Validation/Constraints/WhereNotInConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Spatie\LaravelData\Support\Validation\Constraints;

class WhereNotInConstraint implements DatabaseConstraint
{
public function __construct(
public readonly mixed $column,
public readonly mixed $values,
) {}

public function toArray(): array
{
return [$this->column, $this->values];
}
}
15 changes: 15 additions & 0 deletions src/Support/Validation/Constraints/WhereNotNullConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Spatie\LaravelData\Support\Validation\Constraints;

class WhereNotNullConstraint implements DatabaseConstraint
{
public function __construct(
public readonly mixed $column,
) {}

public function toArray(): array
{
return [$this->column];
}
}
15 changes: 15 additions & 0 deletions src/Support/Validation/Constraints/WhereNullConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Spatie\LaravelData\Support\Validation\Constraints;

class WhereNullConstraint implements DatabaseConstraint
{
public function __construct(
public readonly mixed $column,
) {}

public function toArray(): array
{
return [$this->column];
}
}
Loading
Loading