Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
SoftDelete
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
5 / 5
10
100.00% covered (success)
100.00%
1 / 1
 softDelete
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
6
 modifySoftDeleteQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 beforeSoftDelete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 afterSoftDelete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSoftDeleteUserId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace DevToolbelt\LaravelFastCrud\Actions;
6
7use Carbon\Carbon;
8use Illuminate\Database\Eloquent\Builder;
9use Illuminate\Database\Eloquent\Model;
10use Illuminate\Http\JsonResponse;
11use Illuminate\Support\Str;
12use Psr\Http\Message\ResponseInterface;
13
14/**
15 * Provides the soft delete action for CRUD controllers.
16 *
17 * Soft deletes a model instance by updating configurable fields (deleted_at, deleted_by).
18 * Does not require the model to use Laravel's SoftDeletes trait.
19 *
20 * @method string modelClassName() Returns the Eloquent model class name
21 * @method bool hasModelAttribute(Model $model, string $attributeName) Checks if model has attribute
22 * @method JsonResponse|ResponseInterface answerInvalidUuid() Returns invalid UUID error response
23 * @method JsonResponse|ResponseInterface answerRecordNotFound() Returns not found error response
24 * @method JsonResponse|ResponseInterface answerColumnNotFound(string $field) Returns column not found error response
25 * @method JsonResponse|ResponseInterface answerNoContent() Returns 204 No Content response
26 */
27trait SoftDelete
28{
29    /**
30     * Soft deletes a record by its identifier field.
31     *
32     * @param string $id The identifier value of the record to soft delete
33     * @return JsonResponse|ResponseInterface 204 No Content on success, or error response
34     */
35    public function softDelete(string $id): JsonResponse|ResponseInterface
36    {
37        $findField = config('devToolbelt.fast-crud.soft_delete.find_field')
38            ?? config('devToolbelt.fast-crud.global.find_field', 'id');
39
40        $isUuid = config('devToolbelt.fast-crud.soft_delete.find_field_is_uuid')
41            ?? config('devToolbelt.fast-crud.global.find_field_is_uuid', false);
42
43        $deletedAtField = config('devToolbelt.fast-crud.soft_delete.deleted_at_field', 'deleted_at');
44        $deletedByField = config('devToolbelt.fast-crud.soft_delete.deleted_by_field', 'deleted_by');
45
46        if ($isUuid && !Str::isUuid($id)) {
47            return $this->answerInvalidUuid();
48        }
49
50        $modelName = $this->modelClassName();
51
52        /** @var Model $model */
53        $model = new $modelName();
54
55        if (!$this->hasModelAttribute($model, $deletedAtField)) {
56            return $this->answerColumnNotFound($deletedAtField);
57        }
58
59        if (!$this->hasModelAttribute($model, $deletedByField)) {
60            return $this->answerColumnNotFound($deletedByField);
61        }
62
63        $query = $modelName::query()
64            ->where($findField, $id)
65            ->whereNull($deletedAtField)
66            ->whereNull($deletedByField);
67
68        $this->modifySoftDeleteQuery($query);
69
70        /** @var Model|null $record */
71        $record = $query->first();
72
73        if ($record === null) {
74            return $this->answerRecordNotFound();
75        }
76
77        $this->beforeSoftDelete($record);
78
79        $record->update([
80            $deletedAtField => Carbon::now(),
81            $deletedByField => $this->getSoftDeleteUserId(),
82        ]);
83
84        $this->afterSoftDelete($record);
85
86        return $this->answerNoContent();
87    }
88
89    /**
90     * Hook to modify the soft delete query before fetching the record.
91     *
92     * Override this method to add additional conditions or scopes,
93     * such as ensuring the user owns the record.
94     *
95     * @param Builder $query The query builder instance
96     *
97     * @example
98     * ```php
99     * protected function modifySoftDeleteQuery(Builder $query): void
100     * {
101     *     $query->where('user_id', auth()->id());
102     * }
103     * ```
104     */
105    protected function modifySoftDeleteQuery(Builder $query): void
106    {
107    }
108
109    /**
110     * Hook called before the record is soft deleted.
111     *
112     * Override this method to perform actions before soft deletion,
113     * such as logging, validation, or cleanup of related data.
114     *
115     * @param Model $record The model instance about to be soft deleted
116     */
117    protected function beforeSoftDelete(Model $record): void
118    {
119    }
120
121    /**
122     * Hook called after the record has been soft deleted.
123     *
124     * Override this method to perform post-soft-deletion actions,
125     * such as clearing cache, dispatching events, or audit logging.
126     *
127     * @param Model $record The soft deleted model instance
128     */
129    protected function afterSoftDelete(Model $record): void
130    {
131    }
132
133    /**
134     * Returns the ID of the user performing the soft delete.
135     *
136     * Override this method to provide the authenticated user ID
137     * or any other identifier for audit purposes.
138     *
139     * @return int|string|null The user ID or null if not available
140     *
141     * @example
142     * ```php
143     * protected function getSoftDeleteUserId(): ?int
144     * {
145     *     return auth()->id();
146     * }
147     * ```
148     */
149    protected function getSoftDeleteUserId(): int|string|null
150    {
151        return auth()->user()?->getAuthIdentifier();
152    }
153}