playing_life's avatar

SoftDeletingScope override

I am using Laravel 9 and I want to use Soft Deletes. I have a unique constraint on my table so for the unique to work I need to include the deleted_at column. Because the deleted_at column is Null by default the unique constraint is not working. Ex In MySql, if there is a unique key on Name, the unique constraint is not triggered. Id Name deleted_at

1 Dan null 2 Dan null

What I need is to set the default value of deleted_at to "1970-01-02 00:00:00" and update the code everywhere so in Eloqvent this ("1970-01-02 00:00:00") would be the value that shows that a record is not deleted instead of the value null.

Thank you

0 likes
7 replies
krisi_gjika's avatar
trait AvoidDuplicateConstraintSoftDelete
{
    public static function bootAvoidDuplicateConstraintSoftDelete()
    {
        static::observe(app(UniqueSoftDeleteObserver::class));
    }

    public function getDuplicateAvoidColumns() : array
    {
        return [];
    }
}

class UniqueSoftDeleteObserver
{
    private const DELIMITER = '-#-';

    public function restoring(Model $model)
    {
        if (!$model->trashed()) {
            return;
        }

        foreach ($model->getDuplicateAvoidColumns() as $column) {
            if ($value = (explode(self::DELIMITER, $model->{$column})[1] ?? null)) {
                $model->{$column} = $value;
            }
        }
    }

    public function deleted(Model $model)
    {
        if (!$model->isForceDeleting()) {
            self::mutateColumns($model);
        }
    }

    public static function mutateColumns(Model $model)
    {
        foreach ($model->getDuplicateAvoidColumns() as $column) {
            if (!empty($model->{$column})) {
                $newValue = uniqid() . self::DELIMITER . $model->{$column};
                $model->{$column} = $newValue;
            }
        }

        $model->save();
    }

    public static function restoreColumns(Model $model)
    {
        if (!$model->trashed()) {
            return;
        }

        foreach ($model->getDuplicateAvoidColumns() as $column) {
            if ($value = (explode(self::DELIMITER, $model->{$column})[1] ?? null)) {
                $model->{$column} = $value;
            }
        }

        $model->save();
    }
}

than in your model:

class User extends Model
{
    use HasFactory,
        SoftDeletes,
        AvoidDuplicateConstraintSoftDelete;

  public function getDuplicateAvoidColumns(): array
  {
        return [
            'email',
        ];
  }

now each column defined in getDuplicateAvoidColumns will be mutated to avoid unique constraints, and on restore it will be mutated back to it's original value.

tisuchi's avatar

@playing_life This is how you can set a default value.

Schema::table('your_table_name', function (Blueprint $table) {
    $table->timestamp('deleted_at')->default('1970-01-02 00:00:00');
});
playing_life's avatar

It could be that I still need to polish this but the solution I have found is using the timestamp 1970-01-02 00:00:00 instead of the value Null.

Create file App\Http\Traits\CustomSoftDeleteTrait.php

and

Create file App\Models\Scopes\CustomSoftDeletingScope.php

CustomSoftDeleteTrait.php

namespace App\Http\Traits;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Models\Scopes\CustomSoftDeletingScope;

trait CustomSoftDeletesTrait{
    use SoftDeletes;
    public static function bootSoftDeletes(){
        static::addGlobalScope(new CustomSoftDeletingScope);
    }

    //Override
    protected function restore(){
        if ($this->fireModelEvent('restoring') === false) {
            return false;
        }
        //Changed from original
        $this->{$this->getDeletedAtColumn()} = '1970-01-02 00:00:00';
        //End of change
        $this->exists = true;
        $result = $this->save();
        $this->fireModelEvent('restored', false);
        return $result;
    }

    //Override
    protected function trashed()
    {
        //Changed from original
        return ($this->{$this->getDeletedAtColumn()}) != '1970-01-02 00:00:00';
        //End of change
    }
}

CustomSoftDeletingScope.php

namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\SoftDeletingScope;

class CustomSoftDeletingScope extends SoftDeletingScope implements Scope{
    public function apply(Builder $builder, Model $model){
        $builder->where($model->getQualifiedDeletedAtColumn(), '1970-01-02 00:00:00');
    }

    protected function addWithoutTrashed(Builder $builder){
        $builder->macro('withoutTrashed', function (Builder $builder) {
            $model = $builder->getModel();
            $builder->withoutGlobalScope($this)->where($model->getQualifiedDeletedAtColumn(), '1970-01-02 00:00:00');
            return $builder;
        });
    }
}
newbie360's avatar

in migration use virtualAs() concat the columns and set it to invisable

playing_life's avatar

Another way, which I think it's better, is to create a calculated field called deleted_at which would calculate from the Laravel deleted_at field and instead on NULL to save the string "NULL". Then the deleted_at_calculated field can be added to unique constraints.

Snapey's avatar

in setting a default value you will totally break Eloquent's soft delete implementation

Are you sure you are diagnosing the problem correctly?

A unique index on the name column should not care what the deleted_at column says?

playing_life's avatar

@Snapey I have a unique index on a column. As soon as I use soft deletes on that table, I will have multiple records having that column with the same value. So the unique index will not allow it. If I don't include somehow the deleted_at field in the unique key, it will not work. Using the calculated deleted_at_calculated field work very well and it does not touch anything except the unique key index which needs to be updated.

Please or to participate in this conversation.