Completed
Pull Request — master (#32)
by
unknown
08:14 queued 06:58
created

State::resolveStateMapping()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

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