Completed
Pull Request — master (#32)
by Brent
01:07
created

State::resolveStateName()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.3554
c 0
b 0
f 0
cc 5
nc 5
nop 1
1
<?php
2
3
namespace Spatie\ModelStates;
4
5
use Exception;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Support\Collection;
8
use JsonSerializable;
9
use ReflectionClass;
10
use Spatie\ModelStates\Events\StateChanged;
11
use Spatie\ModelStates\Exceptions\CouldNotPerformTransition;
12
use Spatie\ModelStates\Exceptions\InvalidConfig;
13
14
abstract class State implements JsonSerializable
15
{
16
    /**
17
     * Static cache for generated state maps.
18
     *
19
     * @see State::resolveStateMapping
20
     */
21
    protected static array
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_ARRAY, expecting T_FUNCTION or T_CONST
Loading history...
22
23
 $generatedMapping = [];
24
25
    /** @var \Illuminate\Database\Eloquent\Model */
26
    protected $model;
27
28
    protected ?string $field = null;
29
30
    /**
31
     * Create a state object based on a value (classname or name),
32
     * and optionally provide its constructor arguments.
33
     *
34
     * @param string $name
35
     * @param mixed ...$args
36
     *
37
     * @return \Spatie\ModelStates\State
38
     */
39
    public static function make(string $name, Model $model): State
40
    {
41
        $stateClass = static::resolveStateClass($name);
42
43
        if (! is_subclass_of($stateClass, static::class)) {
44
            throw InvalidConfig::doesNotExtendBaseClass($name, static::class);
45
        }
46
47
        return new $stateClass($model);
48
    }
49
50
    public function __construct(Model $model)
51
    {
52
        $this->model = $model;
53
    }
54
55
    public function setField(string $field): State
56
    {
57
        $this->field = $field;
58
59
        return $this;
60
    }
61
62
    public function getField(): string
63
    {
64
        if (! $this->field) {
65
            throw new Exception("Could not determine the field name of this state class.");
66
        }
67
68
        return $this->field;
69
    }
70
71
    public function getStateConfig(): StateConfig
72
    {
73
        return $this->model::getStateConfig()[$this->field];
74
    }
75
76
    /**
77
     * Create a state object based on a value (classname or name),
78
     * and optionally provide its constructor arguments.
79
     *
80
     * @param string $name
81
     * @param mixed ...$args
82
     *
83
     * @return \Spatie\ModelStates\State
84
     */
85
    public static function find(string $name, Model $model): State
86
    {
87
        return static::make($name, $model);
88
    }
89
90
    /**
91
     * Get all registered state classes.
92
     *
93
     * @return \Illuminate\Support\Collection|string[]|static[] A list of class names.
94
     */
95
    public static function all(): Collection
96
    {
97
        return collect(self::resolveStateMapping());
98
    }
99
100
    /**
101
     * The value that will be saved in the database.
102
     *
103
     * @return string
104
     */
105
    public static function getMorphClass(): string
106
    {
107
        return static::resolveStateName(static::class);
108
    }
109
110
    /**
111
     * The value that will be saved in the database.
112
     *
113
     * @return string
114
     */
115
    public function getValue(): string
116
    {
117
        return static::getMorphClass();
118
    }
119
120
    /**
121
     * Resolve the state class based on a value, for example a stored value in the database.
122
     *
123
     * @param string|\Spatie\ModelStates\State $state
124
     *
125
     * @return string
126
     */
127
    public static function resolveStateClass($state): ?string
128
    {
129
        if ($state === null) {
130
            return null;
131
        }
132
133
        if ($state instanceof State) {
134
            return get_class($state);
135
        }
136
137
        foreach (static::resolveStateMapping() as $stateClass) {
138
            if (! class_exists($stateClass)) {
139
                continue;
140
            }
141
142
            // Loose comparison is needed here in order to support non-string values,
143
            // Laravel casts their database value automatically to strings if we didn't specify the fields in `$casts`.
144
            if (($stateClass::$name ?? null) == $state) {
145
                return $stateClass;
146
            }
147
        }
148
149
        return $state;
150
    }
151
152
    /**
153
     * Resolve the name of the state, which is the value that will be saved in the database.
154
     *
155
     * Possible names are:
156
     *
157
     *    - The classname, if no explicit name is provided
158
     *    - A name provided in the state class as a public static property:
159
     *      `public static $name = 'dummy'`
160
     *
161
     * @param $state
162
     *
163
     * @return string|null
164
     */
165
    public static function resolveStateName($state): ?string
166
    {
167
        if ($state === null) {
168
            return null;
169
        }
170
171
        if ($state instanceof State) {
172
            $stateClass = get_class($state);
173
        } else {
174
            $stateClass = static::resolveStateClass($state);
175
        }
176
177
        if (class_exists($stateClass) && isset($stateClass::$name)) {
178
            return $stateClass::$name;
179
        }
180
181
        return $stateClass;
182
    }
183
184
    /**
185
     * Determine if the current state is one of an arbitrary number of other states.
186
     * This can be either a classname or a name.
187
     *
188
     * @param string|array ...$stateClasses
189
     *
190
     * @return bool
191
     */
192
    public function isOneOf(...$statesNames): bool
193
    {
194
        $statesNames = collect($statesNames)->flatten()->toArray();
195
196
        foreach ($statesNames as $statesName) {
197
            if ($this->equals($statesName)) {
198
                return true;
199
            }
200
        }
201
202
        return false;
203
    }
204
205
    /**
206
     * Determine if the current state equals another.
207
     * This can be either a classname or a name.
208
     *
209
     * @param string|\Spatie\ModelStates\State $state
210
     *
211
     * @return bool
212
     */
213
    public function equals($state): bool
214
    {
215
        return self::resolveStateClass($state)
216
            === self::resolveStateClass($this);
217
    }
218
219
    /**
220
     * Determine if the current state equals another.
221
     * This can be either a classname or a name.
222
     *
223
     * @param string|\Spatie\ModelStates\State $state
224
     *
225
     * @return bool
226
     */
227
    public function is($state): bool
228
    {
229
        return $this->equals($state);
230
    }
231
232
    public function __toString(): string
233
    {
234
        return static::getMorphClass();
235
    }
236
237
    /**
238
     * @param string|\Spatie\ModelStates\Transition $transitionClass
239
     * @param mixed ...$args
240
     *
241
     * @return \Illuminate\Database\Eloquent\Model
242
     */
243
    public function transition($transition, ...$args): Model
244
    {
245
        if (is_string($transition)) {
246
            $transition = new $transition($this->model, ...$args);
247
        }
248
249
        if (method_exists($transition, 'canTransition')) {
250
            if (! $transition->canTransition()) {
251
                throw CouldNotPerformTransition::notAllowed($this->model, $transition);
252
            }
253
        }
254
255
        $mutatedModel = app()->call([$transition, 'handle']);
256
257
        event(new StateChanged($this, $mutatedModel->{$this->field}, $transition, $this->model));
258
259
        return $mutatedModel;
260
    }
261
262
    /**
263
     * @param string|\Spatie\ModelStates\State $state
264
     * @param mixed ...$args
265
     *
266
     * @return \Illuminate\Database\Eloquent\Model
267
     */
268
    public function transitionTo($state, ...$args): Model
269
    {
270
        if (! method_exists($this->model, 'resolveTransitionClass')) {
271
            throw InvalidConfig::resolveTransitionNotFound($this->model);
272
        }
273
274
        $transition = $this->model->resolveTransitionClass(
275
            static::resolveStateClass($this),
276
            static::resolveStateClass($state)
277
        );
278
279
        return $this->transition($transition, ...$args);
280
    }
281
282
    /**
283
     * Check whether the current state can transition to another one
284
     *
285
     * @param string|\Spatie\ModelStates\State $state
286
     *
287
     * @return bool
288
     */
289
    public function canTransitionTo($state): bool
290
    {
291
        return in_array(
292
            static::resolveStateName($state),
293
            $this->transitionableStates()
294
        );
295
    }
296
297
    public function transitionableStates(): array
298
    {
299
        $stateConfig = $this->getStateConfig();
300
301
        return $stateConfig->transitionableStates(get_class($this));
302
    }
303
304
    /**
305
     * This method is used to find all available implementations of a given abstract state class.
306
     * Finding all implementations can be done in two ways:.
307
     *
308
     *    - The developer can define his own mapping directly in abstract state classes
309
     *      via the `protected $states = []` property
310
     *    - If no specific mapping was provided, the same directory where the abstract state class lives
311
     *      is scanned, and all concrete state classes extending the abstract state class will be provided.
312
     *
313
     * @return array
314
     */
315
    private static function resolveStateMapping(): array
316
    {
317
        if (isset(static::$states)) {
318
            return static::$states;
319
        }
320
321
        if (isset(self::$generatedMapping[static::class])) {
322
            return self::$generatedMapping[static::class];
323
        }
324
325
        $reflection = new ReflectionClass(static::class);
326
327
        ['dirname' => $directory] = pathinfo($reflection->getFileName());
328
329
        $files = scandir($directory);
330
331
        unset($files[0], $files[1]);
332
333
        $namespace = $reflection->getNamespaceName();
334
335
        $resolvedStates = [];
336
337
        foreach ($files as $file) {
338
            ['filename' => $className] = pathinfo($file);
339
340
            $stateClass = $namespace . '\\' . $className;
341
342
            if (! is_subclass_of($stateClass, static::class)) {
343
                continue;
344
            }
345
346
            $resolvedStates[] = $stateClass;
347
        }
348
349
        self::$generatedMapping[static::class] = $resolvedStates;
350
351
        return self::$generatedMapping[static::class];
352
    }
353
354
    public function jsonSerialize()
355
    {
356
        return $this->getValue();
357
    }
358
}
359