'John', 'email' => 'john@example.com']); * * // Check for soft-deleted duplicate * $duplicate = $newUser->findSoftDeletedDuplicate(); * * if ($duplicate) { * // Restore with updated attributes * $restored = $newUser->restoreSoftDeletedRecord($duplicate, ['name' => 'John']); * return "Restored previously deleted user with updated details."; * } * * // Otherwise, proceed with normal save * $newUser->save(); * ``` */ trait SoftDeleteResolution { /** * Check if the current model has the SoftDeletes trait * * @return bool */ protected function usesSoftDeletes(): bool { return in_array(SoftDeletes::class, class_uses_recursive($this)); } /** * Get fields that should be considered unique for this model * This method should be implemented in the model using this trait * * @return array An array of field names that should be unique */ protected function uniqueFields(): array { // Default implementation returns empty array // Models should override this method to specify their unique fields return []; } /** * Check if there's a soft-deleted record that would conflict with this one * * @param array $attributes Optional custom attributes to check instead of the model's current attributes * @return Model|null The conflicting soft-deleted model, or null if none exists */ public function findSoftDeletedDuplicate(array $attributes = []): ?Model { // Ensure the model uses soft deletes if (!$this->usesSoftDeletes()) { Log::warning('SoftDeleteResolution trait used on model without SoftDeletes', [ 'model' => get_class($this) ]); return null; } // Get unique fields for this model $uniqueFields = $this->uniqueFields(); if (empty($uniqueFields)) { Log::warning('No unique fields defined for model using SoftDeleteResolution', [ 'model' => get_class($this) ]); return null; } try { // Use provided attributes or current model attributes $checkAttributes = empty($attributes) ? $this->getAttributes() : $attributes; // Build query to find soft-deleted duplicates $query = static::onlyTrashed(); foreach ($uniqueFields as $field) { // Only include fields that exist in checkAttributes and have a value if (array_key_exists($field, $checkAttributes) && !is_null($checkAttributes[$field])) { $query->where($field, $checkAttributes[$field]); } } // Return the first matching soft-deleted record return $query->first(); } catch (\Exception $e) { Log::error('Error checking for soft-deleted duplicates', [ 'model' => get_class($this), 'error' => $e->getMessage(), 'attributes' => $attributes ?: $this->getAttributes() ]); return null; } } /** * Restore a soft-deleted record and optionally update its attributes * * @param Model $model The soft-deleted model to restore * @param array $attributes New attributes to apply during restoration * @return Model|null The restored model, or null if restoration failed */ public function restoreSoftDeletedRecord(Model $model, array $attributes = []): ?Model { // Ensure the model is actually soft-deleted if (!method_exists($model, 'restore') || is_null($model->deleted_at)) { Log::warning('Attempted to restore a model that is not soft-deleted', [ 'model' => get_class($model), 'id' => $model->getKey() ]); return null; } try { // Begin transaction to ensure atomicity DB::beginTransaction(); // Apply any new attributes if (!empty($attributes)) { $model->fill($attributes); } // Restore the model (removes deleted_at timestamp) $model->restore(); // Save any updated attributes if (!empty($attributes)) { $model->save(); } // Commit transaction DB::commit(); Log::info('Successfully restored soft-deleted record', [ 'model' => get_class($model), 'id' => $model->getKey(), 'attributes_updated' => !empty($attributes) ]); return $model; } catch (\Exception $e) { // Rollback transaction on error DB::rollBack(); Log::error('Failed to restore soft-deleted record', [ 'model' => get_class($model), 'id' => $model->getKey(), 'error' => $e->getMessage(), 'attributes' => $attributes ]); return null; } } /** * Detect if a QueryException is due to a unique constraint violation on a soft-deleted record * * @param QueryException $exception The exception to check * @return bool True if the exception is caused by a conflict with a soft-deleted record */ public function isSoftDeletedUniqueConstraintViolation(QueryException $exception): bool { // Check if the model uses soft deletes if (!$this->usesSoftDeletes()) { return false; } // Check if the exception is due to a unique constraint violation // PostgreSQL error code 23505 is for unique_violation $isPgsqlUniqueViolation = $exception->getCode() === '23505'; // SQLite and MySQL use different error codes $isMysqlUniqueViolation = strpos($exception->getMessage(), 'Duplicate entry') !== false; $isSqliteUniqueViolation = strpos($exception->getMessage(), 'UNIQUE constraint failed') !== false; // If it's not a unique constraint violation, return false if (!($isPgsqlUniqueViolation || $isMysqlUniqueViolation || $isSqliteUniqueViolation)) { return false; } // At this point, we have a unique constraint violation // Now we need to check if the conflicting record is soft-deleted $uniqueFields = $this->uniqueFields(); if (empty($uniqueFields)) { return false; } // Check if there's a soft-deleted record with the same unique values return $this->findSoftDeletedDuplicate() !== null; } /** * Attempt to resolve a unique constraint violation by restoring a soft-deleted record * * @param array $attributes Attributes for the new or updated record * @return Model|null The restored model, or null if no resolution was possible */ public function resolveSoftDeletedConflict(array $attributes = []): ?Model { $duplicate = $this->findSoftDeletedDuplicate($attributes); if ($duplicate) { return $this->restoreSoftDeletedRecord($duplicate, $attributes); } return null; } }