Skip to content

Handle unique validation conflicts caused by soft-deleted records#54275

Closed
mohammadrasoulasghari wants to merge 2 commits intolaravel:11.xfrom
mohammadrasoulasghari:feature/unique-soft-deletes
Closed

Handle unique validation conflicts caused by soft-deleted records#54275
mohammadrasoulasghari wants to merge 2 commits intolaravel:11.xfrom
mohammadrasoulasghari:feature/unique-soft-deletes

Conversation

@mohammadrasoulasghari
Copy link
Contributor

Hi Laravel Team 👋,

During my experience working on various projects, I’ve encountered a recurring issue when validating the uniqueness of a field while using Soft Deletes. Specifically, when using Rule::unique for validation, it often leads to unintended conflicts because soft-deleted records are still considered in the validation check. This has caused confusion and bugs in projects where certain records are logically “deleted” but still exist in the database.

To address this issue, I’ve introduced a new method called withSoftDeletes to the Rule::unique class. This method allows developers to exclude soft-deleted records from the unique validation check. By explicitly opting in to this behavior, it ensures that the framework remains opinionated and avoids introducing any breaking changes.

I believe this addition can improve the developer experience by:

  1. Explicit is better than implicit” — This method makes the behavior clear and opt-in, avoiding unexpected side effects.
  2. Don’t repeat yourself (DRY)” — It reduces the repetitive task of manually adding conditions for soft-deleted records.

Usage Example:
Here’s an example of how this method can be used:

use Illuminate\Validation\Rule;

$request->validate([
    'email' => [
        'required',
        'email',
        Rule::unique('users', 'email')->withSoftDeletes(),
    ],
]);

Thank you for considering this PR! 😊

Looking forward to your feedback and suggestions.

Allows excluding soft-deleted records during unique validation.
public function withSoftDeletes()
{
$this->where(function ($query) {
$query->whereNull('deleted_at');
Copy link
Contributor

Choose a reason for hiding this comment

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

This field may be named differently in each model.

@mohammadrasoulasghari mohammadrasoulasghari force-pushed the feature/unique-soft-deletes branch from 6c8d962 to 358a8c2 Compare January 20, 2025 19:17
Ensure the soft delete column can be dynamically determined based on the model.
@mohammadrasoulasghari mohammadrasoulasghari force-pushed the feature/unique-soft-deletes branch from 358a8c2 to 3be1960 Compare January 20, 2025 19:19
@shaedrich
Copy link
Contributor

Doesn't DatabaseRule have a withoutTrashed() method?

/**
* Ignore soft deleted models during the existence check.
*
* @param string $deletedAtColumn
* @return $this
*/
public function withoutTrashed($deletedAtColumn = 'deleted_at')
{
$this->whereNull($deletedAtColumn);
return $this;
}

Comment on lines +85 to 93
$this->where(function ($query) use ($model) {
$deletedAtColumn = $model && method_exists($model, 'getDeletedAtColumn')
? $model->getDeletedAtColumn()
: 'deleted_at';

$query->whereNull($deletedAtColumn);
});

return $this;
Copy link
Contributor

Choose a reason for hiding this comment

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

You could use tap() here:

Suggested change
$this->where(function ($query) use ($model) {
$deletedAtColumn = $model && method_exists($model, 'getDeletedAtColumn')
? $model->getDeletedAtColumn()
: 'deleted_at';
$query->whereNull($deletedAtColumn);
});
return $this;
return tap($this)->where(function ($query) use ($model) {
$deletedAtColumn = $model && method_exists($model, 'getDeletedAtColumn')
? $model->getDeletedAtColumn()
: 'deleted_at';
$query->whereNull($deletedAtColumn);
});

@taylorotwell taylorotwell marked this pull request as draft January 20, 2025 22:22
@mohammadrasoulasghari mohammadrasoulasghari deleted the feature/unique-soft-deletes branch March 10, 2025 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants