Issues (3)

src/Morph.php (1 issue)

Labels
Severity
1
<?php
2
3
namespace Cesargb\Database\Support;
4
5
use Cesargb\Database\Support\Events\RelationMorphFromModelWasCleaned;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Database\Eloquent\Relations\MorphOneOrMany;
8
use Illuminate\Database\Eloquent\Relations\MorphToMany;
9
use Illuminate\Database\Eloquent\Relations\Relation;
10
use Illuminate\Support\Facades\DB;
11
use Illuminate\Support\Facades\Event;
12
use Symfony\Component\Finder\Finder;
13
14
class Morph
15
{
16
    /**
17
     * Delete polymorphic relationships of the single records from Model.
18
     *
19
     * @param  \Illuminate\Database\Eloquent\Model  $model
20
     * @return void
21
     */
22
    public function delete($model)
23
    {
24
        foreach ($this->getValidMorphRelationsFromModel($model) as $relationMorph) {
25
            if ($relationMorph instanceof MorphOneOrMany) {
26
                $relationMorph->delete();
27
            } elseif ($relationMorph instanceof MorphToMany) {
28
                $relationMorph->detach();
29
            }
30
        }
31
    }
32
33
    /**
34
     * Clean residual polymorphic relationships from all Models.
35
     *
36
     * @param  bool  $dryRun
37
     * @return int Num rows was deleted
38
     */
39
    public function cleanResidualAllModels(bool $dryRun = false)
40
    {
41
        $numRowsDeleted = 0;
42
43
        foreach ($this->getCascadeDeleteModels() as $model) {
44
            $numRowsDeleted += $this->cleanResidualByModel($model, $dryRun);
45
        }
46
47
        return $numRowsDeleted;
48
    }
49
50
    /**
51
     * Clean residual polymorphic relationships from a Model.
52
     *
53
     * @param  Model  $model
54
     * @param  bool  $dryRun
55
     * @return int Num rows was deleted
56
     */
57
    public function cleanResidualByModel($model, bool $dryRun = false)
58
    {
59
        $numRowsDeleted = 0;
60
61
        foreach ($this->getValidMorphRelationsFromModel($model) as $relation) {
62
            if ($relation instanceof MorphOneOrMany || $relation instanceof MorphToMany) {
63
                $deleted = $this->queryCleanOrphan($model, $relation, $dryRun);
64
65
                if ($deleted > 0) {
66
                    Event::dispatch(
67
                        new RelationMorphFromModelWasCleaned($model, $relation, $deleted, $dryRun)
68
                    );
69
                }
70
71
                $numRowsDeleted += $deleted;
72
            }
73
        }
74
75
        return $numRowsDeleted;
76
    }
77
78
    /**
79
     * Get the classes that use the trait CascadeDelete.
80
     *
81
     * @return \Illuminate\Database\Eloquent\Model[]
82
     */
83
    protected function getCascadeDeleteModels()
84
    {
85
        $this->load();
86
87
        return array_map(
88
            function ($modelName) {
89
                return new $modelName();
90
            },
91
            $this->getModelsNameWithCascadeDeleteTrait()
92
        );
93
    }
94
95
    /**
96
     * Query to clean orphan morph table.
97
     *
98
     * @param  Model  $parentModel
99
     * @param  MorphOneOrMany|MorphToMany  $relation
100
     * @param  bool  $dryRun
101
     * @return int Num rows was deleted
102
     */
103
    protected function queryCleanOrphan(Model $parentModel, Relation $relation, bool $dryRun = false)
104
    {
105
        [$childTable, $childFieldType, $childFieldId] = $this->getStructureMorphRelation($relation);
106
107
        $method = $dryRun ? 'count' : 'delete';
108
109
        return DB::table($childTable)
110
                ->where($childFieldType, $parentModel->getMorphClass())
111
                ->whereNotExists(function ($query) use (
112
                    $parentModel,
113
                    $childTable,
114
                    $childFieldId
115
                ) {
116
                    $query->select(DB::raw(1))
117
                            ->from($parentModel->getTable())
118
                            ->whereColumn($parentModel->getTable() . '.' . $parentModel->getKeyName(), '=', $childTable . '.' . $childFieldId);
119
                })->$method();
120
    }
121
122
    /**
123
     * Get table and fields from morph relation.
124
     *
125
     * @param  MorphOneOrMany|MorphToMany  $relation
126
     * @return array [$table, $fieldType, $fieldId]
127
     */
128
    protected function getStructureMorphRelation(Relation $relation): array
129
    {
130
        $fieldType = $relation->getMorphType();
131
132
        if ($relation instanceof MorphOneOrMany) {
133
            $table = $relation->getRelated()->getTable();
134
            $fieldId = $relation->getForeignKeyName();
135
        } elseif ($relation instanceof MorphToMany) {
136
            $table = $relation->getTable();
137
            $fieldId = $relation->getForeignPivotKeyName();
138
        } else {
139
            throw new \Exception('Invalid morph relation');
140
        }
141
142
        return [$table, $fieldType, $fieldId];
143
    }
144
145
    /**
146
     * Get the classes names that use the trait CascadeDelete.
147
     *
148
     * @return array
149
     */
150
    protected function getModelsNameWithCascadeDeleteTrait()
151
    {
152
        return array_filter(
153
            get_declared_classes(),
154
            function ($class) {
155
                return array_key_exists(
156
                    CascadeDelete::class,
157
                    class_uses($class)
158
                );
159
            }
160
        );
161
    }
162
163
    /**
164
     * Fetch polymorphic relationships from a Model.
165
     *
166
     * @param  \Illuminate\Database\Eloquent\Model  $model
167
     * @return array
168
     */
169
    protected function getValidMorphRelationsFromModel($model)
170
    {
171
        if (! method_exists($model, 'getCascadeDeleteMorph')) {
172
            return [];
173
        }
174
175
        return array_filter(
176
            array_map(
177
                function ($methodName) use ($model) {
178
                    return $this->methodReturnedMorphRelation($model, $methodName);
179
                },
180
                $model->getCascadeDeleteMorph()
0 ignored issues
show
It seems like $model->getCascadeDeleteMorph() can also be of type Illuminate\Database\Eloquent\Builder and Illuminate\Database\Eloq...gHasThroughRelationship; however, parameter $array of array_map() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

180
                /** @scrutinizer ignore-type */ $model->getCascadeDeleteMorph()
Loading history...
181
            ),
182
            function ($relation) {
183
                return $relation;
184
            }
185
        );
186
    }
187
188
    /**
189
     * Verify if method of a Model return a polymorphic relationship.
190
     *
191
     * @param  \Illuminate\Database\Eloquent\Model  $model
192
     * @param  string  $methodName
193
     * @return bool
194
     */
195
    protected function methodReturnedMorphRelation($model, $methodName)
196
    {
197
        if (! method_exists($model, $methodName)) {
198
            return false;
199
        }
200
201
        $relation = $model->$methodName();
202
203
        return $this->isMorphRelation($relation) ? $relation : null;
204
    }
205
206
    /**
207
     * Verify if a object is a instance of a polymorphic relationship.
208
     *
209
     * @param  mixed  $relation
210
     * @return bool
211
     */
212
    protected function isMorphRelation($relation)
213
    {
214
        return $relation instanceof MorphOneOrMany || $relation instanceof MorphToMany;
215
    }
216
217
    /**
218
     * Load models with Cascade Delete.
219
     *
220
     * @return void
221
     */
222
    protected function load()
223
    {
224
        foreach ($this->findModels() as $file) {
225
            require_once $file->getPathname();
226
        }
227
    }
228
229
    protected function findModels()
230
    {
231
        return Finder::create()
232
            ->files()
233
            ->in(config('morph.models_paths', app_path()))
234
            ->name('*.php')
235
            ->contains('CascadeDelete');
236
    }
237
}
238