Completed
Push — master ( 07e03f...3e53a1 )
by Freek
20s queued 11s
created

src/State.php (1 issue)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 mixed ...$args
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 mixed ...$args
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 (static::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 $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 $transitionClass
0 ignored issues
show
There is no parameter named $transitionClass. Did you maybe mean $transition?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
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
     * @return array
276
     */
277
    public function transitionableStates($field = null): array
278
    {
279
        return $this->model->transitionableStates(get_class($this), $field);
280
    }
281
282
    /**
283
     * This method is used to find all available implementations of a given abstract state class.
284
     * Finding all implementations can be done in two ways:.
285
     *
286
     *    - The developer can define his own mapping directly in abstract state classes
287
     *      via the `protected $states = []` property
288
     *    - If no specific mapping was provided, the same directory where the abstract state class lives
289
     *      is scanned, and all concrete state classes extending the abstract state class will be provided.
290
     *
291
     * @return array
292
     */
293
    private static function resolveStateMapping(): array
294
    {
295
        if (isset(static::$states)) {
296
            return static::$states;
297
        }
298
299
        if (isset(self::$generatedMapping[static::class])) {
300
            return self::$generatedMapping[static::class];
301
        }
302
303
        $reflection = new ReflectionClass(static::class);
304
305
        ['dirname' => $directory] = pathinfo($reflection->getFileName());
306
307
        $files = scandir($directory);
308
309
        unset($files[0], $files[1]);
310
311
        $namespace = $reflection->getNamespaceName();
312
313
        $resolvedStates = [];
314
315
        foreach ($files as $file) {
316
            ['filename' => $className] = pathinfo($file);
317
318
            $stateClass = $namespace.'\\'.$className;
319
320
            if (! is_subclass_of($stateClass, static::class)) {
321
                continue;
322
            }
323
324
            $resolvedStates[] = $stateClass;
325
        }
326
327
        self::$generatedMapping[static::class] = $resolvedStates;
328
329
        return self::$generatedMapping[static::class];
330
    }
331
332
    public function jsonSerialize()
333
    {
334
        return $this->getValue();
335
    }
336
}
337