Completed
Pull Request — master (#86)
by
unknown
01:06
created

State::resolveBaseStateClass()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
cc 4
nc 3
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
            if (($stateClass::$name ?? null) == $state) {
121
                return $stateClass;
122
            }
123
        }
124
125
        return $state;
126
    }
127
128
    /**
129
     * Resolve the name of the state, which is the value that will be saved in the database.
130
     *
131
     * Possible names are:
132
     *
133
     *    - The classname, if no explicit name is provided
134
     *    - A name provided in the state class as a public static property:
135
     *      `public static $name = 'dummy'`
136
     *
137
     * @param string|\Spatie\ModelStates\State $state
138
     *
139
     * @return string|null
140
     */
141
    public static function resolveStateName($state): ?string
142
    {
143
        if ($state === null) {
144
            return null;
145
        }
146
147
        if ($state instanceof State) {
148
            $stateClass = get_class($state);
149
        } else {
150
            $stateClass = static::resolveStateClass($state);
151
        }
152
153
        if (class_exists($stateClass) && isset($stateClass::$name)) {
154
            return $stateClass::$name;
155
        }
156
157
        return $stateClass;
158
    }
159
160
    /**
161
     * Determine if the current state is one of an arbitrary number of other states.
162
     * This can be either a classname or a name.
163
     *
164
     * @param string|array ...$stateClasses
165
     *
166
     * @return bool
167
     */
168
    public function isOneOf(...$statesNames): bool
169
    {
170
        $statesNames = collect($statesNames)->flatten()->toArray();
171
172
        foreach ($statesNames as $statesName) {
173
            if ($this->equals($statesName)) {
174
                return true;
175
            }
176
        }
177
178
        return false;
179
    }
180
181
    /**
182
     * Determine if the current state equals another.
183
     * This can be either a classname or a name.
184
     *
185
     * @param string|\Spatie\ModelStates\State $state
186
     *
187
     * @return bool
188
     */
189
    public function equals($state): bool
190
    {
191
        return self::resolveStateClass($state)
192
            === self::resolveStateClass($this);
193
    }
194
195
    /**
196
     * Determine if the current state equals another.
197
     * This can be either a classname or a name.
198
     *
199
     * @param string|\Spatie\ModelStates\State $state
200
     *
201
     * @return bool
202
     */
203
    public function is($state): bool
204
    {
205
        return $this->equals($state);
206
    }
207
208
    public function __toString(): string
209
    {
210
        return static::getMorphClass();
211
    }
212
213
    /**
214
     * @param string|\Spatie\ModelStates\Transition $transition
215
     * @param mixed ...$args
216
     *
217
     * @return \Illuminate\Database\Eloquent\Model
218
     */
219
    public function transition($transition, ...$args): Model
220
    {
221
        if (is_string($transition)) {
222
            $transition = new $transition($this->model, ...$args);
223
        }
224
225
        if (method_exists($transition, 'canTransition')) {
226
            if (! $transition->canTransition()) {
227
                throw CouldNotPerformTransition::notAllowed($this->model, $transition);
228
            }
229
        }
230
231
        $mutatedModel = app()->call([$transition, 'handle']);
232
233
        /*
234
         * There's a bug with the `finalState` variable:
235
         *      `$mutatedModel->state`
236
         * was used, but this is wrong because we cannot determine the model field within this state class.
237
         * Hence `state` is hardcoded, but that's wrong.
238
         *
239
         * @see https://github.com/spatie/laravel-model-states/issues/49
240
         */
241
        $finalState = $mutatedModel->state;
242
243
        if (! $finalState instanceof State) {
244
            $finalState = null;
245
        }
246
247
        event(new StateChanged($this, $finalState, $transition, $this->model));
248
249
        return $mutatedModel;
250
    }
251
252
    /**
253
     * @param string|\Spatie\ModelStates\State $state
254
     * @param mixed ...$args
255
     *
256
     * @return \Illuminate\Database\Eloquent\Model
257
     */
258
    public function transitionTo($state, ...$args): Model
259
    {
260
        if (! method_exists($this->model, 'resolveTransitionClass')) {
261
            throw InvalidConfig::resolveTransitionNotFound($this->model);
262
        }
263
264
        $transition = $this->model->resolveTransitionClass(
265
            static::resolveStateClass($this),
266
            static::resolveStateClass($state)
267
        );
268
269
        return $this->transition($transition, ...$args);
270
    }
271
272
    /**
273
     * Get the transitionable states from this state.
274
     *
275
     * @param string|null $field
276
     *
277
     * @return array
278
     */
279
    public function transitionableStates($field = null): array
280
    {
281
        return $this->model->transitionableStates(get_class($this), $field);
282
    }
283
284
    /**
285
     * This method is used to find all available implementations of a given abstract state class.
286
     * Finding all implementations can be done in two ways:.
287
     *
288
     *    - The developer can define his own mapping directly in abstract state classes
289
     *      via the `protected $states = []` property
290
     *    - If no specific mapping was provided, the same directory where the abstract state class lives
291
     *      is scanned, and all concrete state classes extending the abstract state class will be provided.
292
     *
293
     * @return array
294
     */
295
    private static function resolveStateMapping(): array
296
    {
297
        if (isset(static::$states)) {
298
            return static::$states;
299
        }
300
301
        $base_class = static::resolveBaseStateClass();
302
303
        if (isset(self::$generatedMapping[$base_class])) {
304
            return self::$generatedMapping[$base_class];
305
        }
306
307
        $reflection = new ReflectionClass($base_class);
308
309
        ['dirname' => $directory] = pathinfo($reflection->getFileName());
0 ignored issues
show
Bug introduced by
The variable $directory does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
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);
0 ignored issues
show
Bug introduced by
The variable $className does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
321
322
            $stateClass = $namespace.'\\'.$className;
323
324
            if (! is_subclass_of($stateClass, $base_class)) {
325
                continue;
326
            }
327
328
            $resolvedStates[] = $stateClass;
329
        }
330
331
        self::$generatedMapping[$base_class] = $resolvedStates;
332
333
        return self::$generatedMapping[$base_class];
334
    }
335
336
    public function jsonSerialize()
337
    {
338
        return $this->getValue();
339
    }
340
341
    /**
342
     * This method is used to find the base state class.
343
     *
344
     * @return string
345
     */
346
    public static function resolveBaseStateClass(): string
347
    {
348
        $reflection = new ReflectionClass(static::class);
349
350
        if ($reflection->isAbstract() || optional($reflection->getParentClass())->name === self::class) {
351
            return static::class;
352
        }
353
354
        if ($reflection->getParentClass() !== false) {
355
            return call_user_func([$reflection->getParentClass()->name, __FUNCTION__]);
356
        }
357
358
        throw new \Exception('Unable to resolve the classname for ' . static::class);
359
    }
360
}
361