Completed
Pull Request — master (#107)
by
unknown
10:55
created

HasStates::getDefaultStateFor()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
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
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);
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);
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);
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);
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);
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);
159
        }
160
161
        $field = $field ?? reset($stateConfig)->field;
162
163
        if (! array_key_exists($field, $stateConfig)) {
164
            throw InvalidConfig::unknownState($field, $this);
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);
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $transitionClass of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
212
                return $transitionClass;
213
            }
214
        }
215
216
        throw CouldNotPerformTransition::notFound($fromClass, $toClass, $this);
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
    /**
269
     * Determine if the new and old values for a given key are equivalent.
270
     *
271
     * @param  string  $key
272
     * @return bool
273
     */
274
    public function originalIsEquivalent($key)
275
    {
276
        $original = Arr::get($this->original, $key);
0 ignored issues
show
Bug introduced by
The property original does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
277
278
        if ($original instanceof State) {
279
            $attribute = $original::resolveStateName(Arr::get($this->attributes, $key));
0 ignored issues
show
Bug introduced by
The property attributes does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
280
            $original = $original::resolveStateName($original);
281
282
            if ($original === $attribute) {
283
                return true;
284
            }
285
        }
286
287
        return parent::originalIsEquivalent($key);
288
    }
289
}
290