HasStates   A
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 248
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 36
eloc 100
c 4
b 0
f 0
dl 0
loc 248
rs 9.52

14 Methods

Rating   Name   Duplication   Size   Complexity  
A scopeWhereState() 0 18 2
A getDefaultStateFor() 0 3 1
A resolveTransitionClass() 0 11 3
A addState() 0 7 1
A getStatesFor() 0 3 1
A getStates() 0 5 1
B bootHasStates() 0 57 7
A transitionTo() 0 11 3
A scopeWhereNotState() 0 16 2
A canTransitionTo() 0 22 5
A getDefaultStates() 0 5 1
A transitionableStates() 0 15 4
A getStateConfig() 0 9 2
A initializeHasStates() 0 8 3
1
<?php
2
3
namespace Spatie\ModelStates;
4
5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Support\Arr;
8
use Illuminate\Support\Collection;
9
use Spatie\ModelStates\Exceptions\CouldNotPerformTransition;
10
use Spatie\ModelStates\Exceptions\InvalidConfig;
11
12
/**
13
 * @mixin \Illuminate\Database\Eloquent\Model
14
 *
15
 * @method static Builder whereState(string $field, string|string[] $states)
16
 * @method static Builder whereNotState(string $field, string|string[] $states)
17
 */
18
trait HasStates
19
{
20
    /** @var \Spatie\ModelStates\StateConfig[]|null */
21
    protected static $stateFields = null;
22
23
    abstract protected function registerStates(): void;
24
25
    public static function bootHasStates(): void
26
    {
27
        $serializeState = function (StateConfig $stateConfig) {
28
            return function (Model $model) use ($stateConfig) {
29
                $value = $model->getAttribute($stateConfig->field);
30
31
                if ($value === null) {
32
                    $value = $stateConfig->defaultStateClass;
33
                }
34
35
                if ($value === null) {
36
                    return;
37
                }
38
39
                $stateClass = $stateConfig->stateClass::resolveStateClass($value);
40
41
                if (! is_subclass_of($stateClass, $stateConfig->stateClass)) {
42
                    throw InvalidConfig::fieldDoesNotExtendState(
43
                        $stateConfig->field,
44
                        $stateConfig->stateClass,
45
                        $stateClass
0 ignored issues
show
Bug introduced by
It seems like $stateClass can also be of type null; however, parameter $actualClass of Spatie\ModelStates\Excep...eldDoesNotExtendState() does only seem to accept string, 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

45
                        /** @scrutinizer ignore-type */ $stateClass
Loading history...
46
                    );
47
                }
48
49
                $model->setAttribute(
50
                    $stateConfig->field,
51
                    State::resolveStateName($value)
52
                );
53
            };
54
        };
55
56
        $unserializeState = function (StateConfig $stateConfig) {
57
            return function (Model $model) use ($stateConfig) {
58
                $stateClass = $stateConfig->stateClass::resolveStateClass($model->getAttribute($stateConfig->field));
59
60
                $defaultState = $stateConfig->defaultStateClass
61
                    ? new $stateConfig->defaultStateClass($model)
62
                    : null;
63
64
                $model->setAttribute(
65
                    $stateConfig->field,
66
                    class_exists($stateClass)
67
                        ? new $stateClass($model)
68
                        : $defaultState
69
                );
70
            };
71
        };
72
73
        foreach (self::getStateConfig() as $stateConfig) {
74
            static::retrieved($unserializeState($stateConfig));
75
            static::created($unserializeState($stateConfig));
76
            static::updated($unserializeState($stateConfig));
77
            static::saved($unserializeState($stateConfig));
78
79
            static::updating($serializeState($stateConfig));
80
            static::creating($serializeState($stateConfig));
81
            static::saving($serializeState($stateConfig));
82
        }
83
    }
84
85
    public function initializeHasStates(): void
86
    {
87
        foreach (self::getStateConfig() as $stateConfig) {
88
            if (! $stateConfig->defaultStateClass) {
89
                continue;
90
            }
91
92
            $this->{$stateConfig->field} = new $stateConfig->defaultStateClass($this);
93
        }
94
    }
95
96
    public function scopeWhereState(Builder $builder, string $column, $states): Builder
97
    {
98
        $field = Arr::last(explode('.', $column));
99
100
        /** @var \Spatie\ModelStates\StateConfig|null $stateConfig */
101
        $stateConfig = self::getStateConfig()[$field] ?? null;
102
103
        if (! $stateConfig) {
104
            throw InvalidConfig::unknownState($field, $this);
0 ignored issues
show
Bug introduced by
$this of type Spatie\ModelStates\HasStates is incompatible with the type Illuminate\Database\Eloquent\Model expected by parameter $model of Spatie\ModelStates\Excep...dConfig::unknownState(). ( Ignorable by Annotation )

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

104
            throw InvalidConfig::unknownState($field, /** @scrutinizer ignore-type */ $this);
Loading history...
105
        }
106
107
        $abstractStateClass = $stateConfig->stateClass;
108
109
        $stateNames = collect((array) $states)->map(function ($state) use ($abstractStateClass) {
110
            return $abstractStateClass::resolveStateName($state);
111
        });
112
113
        return $builder->whereIn($column ?? $column, $stateNames);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $builder->whereIn...? $column, $stateNames) could return the type Illuminate\Database\Query\Builder which is incompatible with the type-hinted return Illuminate\Database\Eloquent\Builder. Consider adding an additional type-check to rule them out.
Loading history...
114
    }
115
116
    public function scopeWhereNotState(Builder $builder, string $column, $states): Builder
117
    {
118
        $field = Arr::last(explode('.', $column));
119
120
        /** @var \Spatie\ModelStates\StateConfig|null $stateConfig */
121
        $stateConfig = self::getStateConfig()[$field] ?? null;
122
123
        if (! $stateConfig) {
124
            throw InvalidConfig::unknownState($field, $this);
0 ignored issues
show
Bug introduced by
$this of type Spatie\ModelStates\HasStates is incompatible with the type Illuminate\Database\Eloquent\Model expected by parameter $model of Spatie\ModelStates\Excep...dConfig::unknownState(). ( Ignorable by Annotation )

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

124
            throw InvalidConfig::unknownState($field, /** @scrutinizer ignore-type */ $this);
Loading history...
125
        }
126
127
        $stateNames = collect((array) $states)->map(function ($state) use ($stateConfig) {
128
            return $stateConfig->stateClass::resolveStateName($state);
129
        });
130
131
        return $builder->whereNotIn($column ?? $column, $stateNames);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $builder->whereNo...? $column, $stateNames) could return the type Illuminate\Database\Query\Builder which is incompatible with the type-hinted return Illuminate\Database\Eloquent\Builder. Consider adding an additional type-check to rule them out.
Loading history...
132
    }
133
134
    /**
135
     * @param \Spatie\ModelStates\State|string $state
136
     * @param string|null $field
137
     *
138
     * @return \Illuminate\Database\Eloquent\Model
139
     */
140
    public function transitionTo($state, string $field = null)
141
    {
142
        $stateConfig = self::getStateConfig();
143
144
        if ($field === null && count($stateConfig) > 1) {
145
            throw CouldNotPerformTransition::couldNotResolveTransitionField($this);
0 ignored issues
show
Bug introduced by
$this of type Spatie\ModelStates\HasStates is incompatible with the type Illuminate\Database\Eloquent\Model expected by parameter $model of Spatie\ModelStates\Excep...esolveTransitionField(). ( Ignorable by Annotation )

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

145
            throw CouldNotPerformTransition::couldNotResolveTransitionField(/** @scrutinizer ignore-type */ $this);
Loading history...
146
        }
147
148
        $field = $field ?? reset($stateConfig)->field;
149
150
        return $this->{$field}->transitionTo($state);
151
    }
152
153
    public function transitionableStates(string $fromClass, ?string $field = null): array
154
    {
155
        $stateConfig = self::getStateConfig();
156
157
        if ($field === null && count($stateConfig) > 1) {
158
            throw InvalidConfig::fieldNotFound($fromClass, $this);
0 ignored issues
show
Bug introduced by
$this of type Spatie\ModelStates\HasStates is incompatible with the type Illuminate\Database\Eloquent\Model expected by parameter $model of Spatie\ModelStates\Excep...Config::fieldNotFound(). ( Ignorable by Annotation )

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

158
            throw InvalidConfig::fieldNotFound($fromClass, /** @scrutinizer ignore-type */ $this);
Loading history...
159
        }
160
161
        $field = $field ?? reset($stateConfig)->field;
162
163
        if (! array_key_exists($field, $stateConfig)) {
164
            throw InvalidConfig::unknownState($field, $this);
0 ignored issues
show
Bug introduced by
$this of type Spatie\ModelStates\HasStates is incompatible with the type Illuminate\Database\Eloquent\Model expected by parameter $model of Spatie\ModelStates\Excep...dConfig::unknownState(). ( Ignorable by Annotation )

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

164
            throw InvalidConfig::unknownState($field, /** @scrutinizer ignore-type */ $this);
Loading history...
165
        }
166
167
        return $stateConfig[$field]->transitionableStates($fromClass);
168
    }
169
170
    /**
171
     * @param \Spatie\ModelStates\State|string $to
172
     * @param string|null $field
173
     *
174
     * @return bool
175
     */
176
    public function canTransitionTo($to, ?string $field = null): bool
177
    {
178
        $statesConfig = self::getStateConfig();
179
180
        if ($field === null && count($statesConfig) > 1) {
181
            throw InvalidConfig::fieldNotFound(($to instanceof State) ? get_class($to) : $to, $this);
0 ignored issues
show
Bug introduced by
$this of type Spatie\ModelStates\HasStates is incompatible with the type Illuminate\Database\Eloquent\Model expected by parameter $model of Spatie\ModelStates\Excep...Config::fieldNotFound(). ( Ignorable by Annotation )

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

181
            throw InvalidConfig::fieldNotFound(($to instanceof State) ? get_class($to) : $to, /** @scrutinizer ignore-type */ $this);
Loading history...
182
        }
183
184
        $field = $field ?? reset($statesConfig)->field;
185
186
        $stateConfig = $statesConfig[$field];
187
188
        try {
189
            $this->resolveTransitionClass(
190
                $stateConfig->stateClass::resolveStateClass($this->$field),
191
                $stateConfig->stateClass::resolveStateClass($to)
192
            );
193
        } catch (CouldNotPerformTransition $exception) {
194
            return false;
195
        }
196
197
        return true;
198
    }
199
200
    /**
201
     * @param string $fromClass
202
     * @param string $toClass
203
     *
204
     * @return \Spatie\ModelStates\Transition|string|null
205
     */
206
    public function resolveTransitionClass(string $fromClass, string $toClass)
207
    {
208
        foreach (static::getStateConfig() as $stateConfig) {
209
            $transitionClass = $stateConfig->resolveTransition($this, $fromClass, $toClass);
210
211
            if ($transitionClass) {
212
                return $transitionClass;
213
            }
214
        }
215
216
        throw CouldNotPerformTransition::notFound($fromClass, $toClass, $this);
0 ignored issues
show
Bug introduced by
$this of type Spatie\ModelStates\HasStates is incompatible with the type Illuminate\Database\Eloquent\Model expected by parameter $model of Spatie\ModelStates\Excep...mTransition::notFound(). ( Ignorable by Annotation )

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

216
        throw CouldNotPerformTransition::notFound($fromClass, $toClass, /** @scrutinizer ignore-type */ $this);
Loading history...
217
    }
218
219
    protected function addState(string $field, string $stateClass): StateConfig
220
    {
221
        $stateConfig = new StateConfig($field, $stateClass);
222
223
        static::$stateFields[$stateConfig->field] = $stateConfig;
224
225
        return $stateConfig;
226
    }
227
228
    /**
229
     * @return \Spatie\ModelStates\StateConfig[]
230
     */
231
    private static function getStateConfig(): array
232
    {
233
        if (static::$stateFields === null) {
234
            static::$stateFields = [];
235
236
            (new static)->registerStates();
237
        }
238
239
        return static::$stateFields ?? [];
240
    }
241
242
    public static function getStates(): Collection
243
    {
244
        return collect(static::getStateConfig())
245
            ->map(function ($state) {
246
                return $state->stateClass::all();
247
            });
248
    }
249
250
    public static function getStatesFor(string $column): Collection
251
    {
252
        return static::getStates()->get($column, new Collection);
253
    }
254
255
    public static function getDefaultStates(): Collection
256
    {
257
        return collect(static::getStateConfig())
258
            ->map(function ($state) {
259
                return $state->defaultStateClass;
260
            });
261
    }
262
263
    public static function getDefaultStateFor(string $column): string
264
    {
265
        return static::getDefaultStates()->get($column);
266
    }
267
}
268