CascadesDeletes::getCascadeDeletesRelationNames()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
cc 2
nc 1
nop 0
crap 2
1
<?php
2
3
namespace ShiftOneLabs\LaravelCascadeDeletes;
4
5
use LogicException;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Database\Eloquent\Relations\Relation;
8
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
9
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
10
11
trait CascadesDeletes
12
{
13
    /**
14
     * Use the boot function to setup model event listeners.
15
     *
16
     * @return void
17
     */
18 192
    public static function bootCascadesDeletes()
19
    {
20
        // Setup the 'deleting' event listener.
21 192
        static::deleting(function ($model) {
22
23
            // Wrap all of the cascading deletes inside of a transaction to make this an
24
            // all or nothing operation. Any exceptions thrown inside the transaction
25
            // need to bubble up to make sure all transactions will be rolled back.
26 192
            $model->getConnectionResolver()->transaction(function () use ($model) {
27
28 192
                $relations = $model->getCascadeDeletesRelations();
29
30 192
                if ($invalidRelations = $model->getInvalidCascadeDeletesRelations($relations)) {
31 12
                    throw new LogicException(sprintf('[%s]: invalid relationship(s) for cascading deletes. Relationship method(s) [%s] must return an object of type Illuminate\Database\Eloquent\Relations\Relation.', static::class, implode(', ', $invalidRelations)));
32
                }
33
34 180
                $deleteMethod = $model->isCascadeDeletesForceDeleting() ? 'forceDelete' : 'delete';
35
36 180
                foreach ($relations as $relationName => $relation) {
37 180
                    $expected = 0;
38 180
                    $deleted = 0;
39
40 180
                    if ($relation instanceof BelongsToMany) {
41
                        // Process the many-to-many relationships on the model.
42
                        // These relationships should not delete the related
43
                        // record, but should just detach from each other.
44
45 156
                        $expected = $model->getCascadeDeletesRelationQuery($relationName)->count();
46
47 156
                        $deleted = $model->getCascadeDeletesRelationQuery($relationName)->detach();
48 180
                    } elseif ($relation instanceof HasOneOrMany) {
49
                        // Process the one-to-one and one-to-many relationships
50
                        // on the model. These relationships should actually
51
                        // delete the related records from the database.
52
53 168
                        $children = $model->getCascadeDeletesRelationQuery($relationName)->get();
54
55
                        // To protect against potential relationship defaults,
56
                        // filter out any children that may not actually be
57
                        // Model instances, or that don't actually exist.
58 168
                        $children = $children->filter(function ($child) {
59 168
                            return $child instanceof Model && $child->exists;
60 168
                        })->all();
61
62 168
                        $expected = count($children);
63
64 168
                        foreach ($children as $child) {
65
                            // Delete the record using the proper method.
66 168
                            $deleted += $child->$deleteMethod();
67
                        }
68
                    } else {
69
                        // Not all relationship types make sense for cascading. As an
70
                        // example, for a BelongsTo relationship, it does not make
71
                        // sense to delete the parent when the child is deleted.
72 48
                        throw new LogicException(sprintf('[%s]: error occurred deleting [%s]. Relation type [%s] not handled.', static::class, $relationName, get_class($relation)));
73
                    }
74
75 168
                    if ($deleted < $expected) {
76 12
                        throw new LogicException(sprintf('[%s]: error occurred deleting [%s]. Only deleted [%d] out of [%d] records.', static::class, $relationName, $deleted, $expected));
77
                    }
78
                }
79 192
            });
80 192
        });
81
    }
82
83
    /**
84
     * Get the value of the cascadeDeletes attribute, if it exists.
85
     *
86
     * @return mixed
87
     */
88 408
    public function getCascadeDeletes()
89
    {
90 408
        return property_exists($this, 'cascadeDeletes') ? $this->cascadeDeletes : [];
91
    }
92
93
    /**
94
     * Set the cascadeDeletes attribute.
95
     *
96
     * @param  mixed  $cascadeDeletes
97
     *
98
     * @return void
99
     */
100 180
    public function setCascadeDeletes($cascadeDeletes)
101
    {
102 180
        $this->cascadeDeletes = $cascadeDeletes;
0 ignored issues
show
Bug Best Practice introduced by
The property cascadeDeletes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
103
    }
104
105
    /**
106
     * Get an array of cascading relation names.
107
     *
108
     * @return array
109
     */
110 384
    public function getCascadeDeletesRelationNames()
111
    {
112 384
        $deletes = $this->getCascadeDeletes();
113
114 384
        return array_filter(is_array($deletes) ? $deletes : [$deletes]);
115
    }
116
117
    /**
118
     * Get an array of the cascading relation names mapped to their relation types.
119
     *
120
     * @return array
121
     */
122 252
    public function getCascadeDeletesRelations()
123
    {
124 252
        $names = $this->getCascadeDeletesRelationNames();
125
126 252
        return array_combine($names, array_map(function ($name) {
127 252
            $relation = method_exists($this, $name) ? $this->$name() : null;
128
129 252
            return $relation instanceof Relation ? $relation : null;
130 252
        }, $names));
131
    }
132
133
    /**
134
     * Get an array of the invalid cascading relation names.
135
     *
136
     * @param  array|null  $relations
137
     *
138
     * @return array
139
     */
140 216
    public function getInvalidCascadeDeletesRelations(?array $relations = null)
141
    {
142
        // This will get the array keys for any item in the array where the
143
        // value is null. If the value is null, that means that the name
144
        // of the relation provided does not return a Relation object.
145 216
        return array_keys($relations ?: $this->getCascadeDeletesRelations(), null);
146
    }
147
148
    /**
149
     * Get the relationship query to use for the specified relation.
150
     *
151
     * @param  string  $relation
152
     *
153
     * @return \Illuminate\Database\Eloquent\Relations\Relation
154
     */
155 204
    public function getCascadeDeletesRelationQuery($relation)
156
    {
157 204
        $query = $this->$relation();
158
159
        // If this is a force delete and the related model is using soft deletes,
160
        // we need to use the withTrashed() scope on the relationship query to
161
        // ensure all related records, plus soft deleted, are force deleted.
162 204
        if ($this->isCascadeDeletesForceDeleting() && !is_null($query->getMacro('withTrashed'))) {
163 60
            $query = $query->withTrashed();
164
        }
165
166 204
        return $query;
167
    }
168
169
    /**
170
     * Check if this cascading delete is a force delete.
171
     *
172
     * @return boolean
173
     */
174 240
    public function isCascadeDeletesForceDeleting()
175
    {
176 240
        return property_exists($this, 'forceDeleting') && $this->forceDeleting;
177
    }
178
}
179