State::getMorphClass()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Spatie\ModelStates;
4
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Support\Collection;
7
use JsonSerializable;
8
use ReflectionClass;
9
use Spatie\ModelStates\Events\StateChanged;
10
use Spatie\ModelStates\Exceptions\CouldNotPerformTransition;
11
use Spatie\ModelStates\Exceptions\InvalidConfig;
12
13
abstract class State implements JsonSerializable
14
{
15
    /**
16
     * Static cache for generated state maps.
17
     *
18
     * @var array
19
     *
20
     * @see State::resolveStateMapping
21
     */
22
    protected static $generatedMapping = [];
23
24
    /** @var \Illuminate\Database\Eloquent\Model */
25
    protected $model;
26
27
    public function __construct(Model $model)
28
    {
29
        $this->model = $model;
30
    }
31
32
    /**
33
     * Create a state object based on a value (classname or name),
34
     * and optionally provide its constructor arguments.
35
     *
36
     * @param string $name
37
     * @param \Illuminate\Database\Eloquent\Model $model
38
     *
39
     * @return \Spatie\ModelStates\State
40
     */
41
    public static function make(string $name, Model $model): State
42
    {
43
        $stateClass = static::resolveStateClass($name);
44
45
        if (! is_subclass_of($stateClass, static::class)) {
46
            throw InvalidConfig::doesNotExtendBaseClass($name, static::class);
47
        }
48
49
        return new $stateClass($model);
50
    }
51
52
    /**
53
     * Create a state object based on a value (classname or name),
54
     * and optionally provide its constructor arguments.
55
     *
56
     * @param string $name
57
     * @param \Illuminate\Database\Eloquent\Model $model
58
     *
59
     * @return \Spatie\ModelStates\State
60
     */
61
    public static function find(string $name, Model $model): State
62
    {
63
        return static::make($name, $model);
64
    }
65
66
    /**
67
     * Get all registered state classes.
68
     *
69
     * @return \Illuminate\Support\Collection|string[]|static[] A list of class names.
70
     */
71
    public static function all(): Collection
72
    {
73
        return collect(self::resolveStateMapping());
74
    }
75
76
    /**
77
     * The value that will be saved in the database.
78
     *
79
     * @return string
80
     */
81
    public static function getMorphClass(): string
82
    {
83
        return static::resolveStateName(static::class);
84
    }
85
86
    /**
87
     * The value that will be saved in the database.
88
     *
89
     * @return string
90
     */
91
    public function getValue(): string
92
    {
93
        return static::getMorphClass();
94
    }
95
96
    /**
97
     * Resolve the state class based on a value, for example a stored value in the database.
98
     *
99
     * @param string|\Spatie\ModelStates\State $state
100
     *
101
     * @return string
102
     */
103
    public static function resolveStateClass($state): ?string
104
    {
105
        if ($state === null) {
106
            return null;
107
        }
108
109
        if ($state instanceof State) {
110
            return get_class($state);
111
        }
112
113
        foreach (self::resolveStateMapping() as $stateClass) {
114
            if (! class_exists($stateClass)) {
115
                continue;
116
            }
117
118
            // Loose comparison is needed here in order to support non-string values,
119
            // Laravel casts their database value automatically to strings if we didn't specify the fields in `$casts`.
120
            $name = isset($stateClass::$name) ? (string) $stateClass::$name : null;
121
122
            if ($name == $state) {
123
                return $stateClass;
124
            }
125
        }
126
127
        return $state;
128
    }
129
130
    /**
131
     * Resolve the name of the state, which is the value that will be saved in the database.
132
     *
133
     * Possible names are:
134
     *
135
     *    - The classname, if no explicit name is provided
136
     *    - A name provided in the state class as a public static property:
137
     *      `public static $name = 'dummy'`
138
     *
139
     * @param string|\Spatie\ModelStates\State $state
140
     *
141
     * @return string|null
142
     */
143
    public static function resolveStateName($state): ?string
144
    {
145
        if ($state === null) {
146
            return null;
147
        }
148
149
        if ($state instanceof State) {
150
            $stateClass = get_class($state);
151
        } else {
152
            $stateClass = static::resolveStateClass($state);
153
        }
154
155
        if (class_exists($stateClass) && isset($stateClass::$name)) {
0 ignored issues
show
Bug introduced by
The property name does not exist on string.
Loading history...
156
            return $stateClass::$name;
157
        }
158
159
        return $stateClass;
160
    }
161
162
    /**
163
     * Determine if the current state is one of an arbitrary number of other states.
164
     * This can be either a classname or a name.
165
     *
166
     * @param string|array ...$stateClasses
167
     *
168
     * @return bool
169
     */
170
    public function isOneOf(...$statesNames): bool
171
    {
172
        $statesNames = collect($statesNames)->flatten()->toArray();
173
174
        foreach ($statesNames as $statesName) {
175
            if ($this->equals($statesName)) {
176
                return true;
177
            }
178
        }
179
180
        return false;
181
    }
182
183
    /**
184
     * Determine if the current state equals another.
185
     * This can be either a classname or a name.
186
     *
187
     * @param string|\Spatie\ModelStates\State $state
188
     *
189
     * @return bool
190
     */
191
    public function equals($state): bool
192
    {
193
        return self::resolveStateClass($state)
194
            === self::resolveStateClass($this);
195
    }
196
197
    /**
198
     * Determine if the current state equals another.
199
     * This can be either a classname or a name.
200
     *
201
     * @param string|\Spatie\ModelStates\State $state
202
     *
203
     * @return bool
204
     */
205
    public function is($state): bool
206
    {
207
        return $this->equals($state);
208
    }
209
210
    public function __toString(): string
211
    {
212
        return static::getMorphClass();
213
    }
214
215
    /**
216
     * @param string|\Spatie\ModelStates\Transition $transition
217
     * @param mixed ...$args
218
     *
219
     * @return \Illuminate\Database\Eloquent\Model
220
     */
221
    public function transition($transition, ...$args): Model
222
    {
223
        if (is_string($transition)) {
224
            $transition = new $transition($this->model, ...$args);
225
        }
226
227
        if (method_exists($transition, 'canTransition')) {
228
            if (! $transition->canTransition()) {
229
                throw CouldNotPerformTransition::notAllowed($this->model, $transition);
230
            }
231
        }
232
233
        $mutatedModel = app()->call([$transition, 'handle']);
234
235
        /*
236
         * There's a bug with the `finalState` variable:
237
         *      `$mutatedModel->state`
238
         * was used, but this is wrong because we cannot determine the model field within this state class.
239
         * Hence `state` is hardcoded, but that's wrong.
240
         *
241
         * @see https://github.com/spatie/laravel-model-states/issues/49
242
         */
243
        $finalState = $mutatedModel->state;
244
245
        if (! $finalState instanceof State) {
246
            $finalState = null;
247
        }
248
249
        event(new StateChanged($this, $finalState, $transition, $this->model));
250
251
        return $mutatedModel;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $mutatedModel could return the type callable which is incompatible with the type-hinted return Illuminate\Database\Eloquent\Model. Consider adding an additional type-check to rule them out.
Loading history...
252
    }
253
254
    /**
255
     * @param string|\Spatie\ModelStates\State $state
256
     * @param mixed ...$args
257
     *
258
     * @return \Illuminate\Database\Eloquent\Model
259
     */
260
    public function transitionTo($state, ...$args): Model
261
    {
262
        if (! method_exists($this->model, 'resolveTransitionClass')) {
263
            throw InvalidConfig::resolveTransitionNotFound($this->model);
264
        }
265
266
        $transition = $this->model->resolveTransitionClass(
267
            static::resolveStateClass($this),
268
            static::resolveStateClass($state)
269
        );
270
271
        return $this->transition($transition, ...$args);
0 ignored issues
show
Bug introduced by
It seems like $transition can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $transition of Spatie\ModelStates\State::transition() does only seem to accept Spatie\ModelStates\Transition|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

271
        return $this->transition(/** @scrutinizer ignore-type */ $transition, ...$args);
Loading history...
272
    }
273
274
    /**
275
     * Get the transitionable states from this state.
276
     *
277
     * @param string|null $field
278
     *
279
     * @return array
280
     */
281
    public function transitionableStates($field = null): array
282
    {
283
        return $this->model->transitionableStates(get_class($this), $field);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->model->tra...t_class($this), $field) could return the type Illuminate\Database\Eloquent\Builder which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
284
    }
285
286
    /**
287
     * This method is used to find all available implementations of a given abstract state class.
288
     * Finding all implementations can be done in two ways:.
289
     *
290
     *    - The developer can define his own mapping directly in abstract state classes
291
     *      via the `protected $states = []` property
292
     *    - If no specific mapping was provided, the same directory where the abstract state class lives
293
     *      is scanned, and all concrete state classes extending the abstract state class will be provided.
294
     *
295
     * @return array
296
     */
297
    private static function resolveStateMapping(): array
298
    {
299
        if (isset(static::$states)) {
300
            return static::$states;
301
        }
302
303
        if (isset(self::$generatedMapping[static::class])) {
304
            return self::$generatedMapping[static::class];
305
        }
306
307
        $reflection = new ReflectionClass(static::class);
308
309
        ['dirname' => $directory] = pathinfo($reflection->getFileName());
310
311
        $files = scandir($directory);
312
313
        unset($files[0], $files[1]);
314
315
        $namespace = $reflection->getNamespaceName();
316
317
        $resolvedStates = [];
318
319
        foreach ($files as $file) {
320
            ['filename' => $className] = pathinfo($file);
321
322
            $stateClass = $namespace.'\\'.$className;
323
324
            if (! is_subclass_of($stateClass, static::class)) {
325
                continue;
326
            }
327
328
            $resolvedStates[] = $stateClass;
329
        }
330
331
        self::$generatedMapping[static::class] = $resolvedStates;
332
333
        return self::$generatedMapping[static::class];
334
    }
335
336
    public function jsonSerialize()
337
    {
338
        return $this->getValue();
339
    }
340
}
341