Completed
Pull Request — master (#31)
by Brent
01:11
created

State::isOneOf()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 3
nc 3
nop 1
1
<?php
2
3
namespace Spatie\ModelStates;
4
5
use Exception;
6
use ReflectionClass;
7
use JsonSerializable;
8
use Illuminate\Support\Collection;
9
use Illuminate\Database\Eloquent\Model;
10
use Spatie\ModelStates\Events\StateChanged;
11
use Spatie\ModelStates\Exceptions\InvalidConfig;
12
use Spatie\ModelStates\Exceptions\CouldNotPerformTransition;
13
14
abstract class State implements JsonSerializable
15
{
16
    /**
17
     * Static cache for generated state maps.
18
     *
19
     * @var array
20
     *
21
     * @see State::resolveStateMapping
22
     */
23
    protected static $generatedMapping = [];
24
25
    /** @var \Illuminate\Database\Eloquent\Model */
26
    protected $model;
27
28
    /** @var string|null */
29
    protected $field;
30
31
    public function __construct(Model $model)
32
    {
33
        $this->model = $model;
34
    }
35
36
    public function setField(string $field): State
37
    {
38
        $this->field = $field;
39
40
        return $this;
41
    }
42
43
    public function getField(): string
44
    {
45
        if (! $this->field) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->field of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
46
            throw new Exception("Could not determine the field name of this state class.");
47
        }
48
49
        return $this->field;
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 make(string $name, Model $model): State
62
    {
63
        $stateClass = static::resolveStateClass($name);
64
65
        if (! is_subclass_of($stateClass, static::class)) {
66
            throw InvalidConfig::doesNotExtendBaseClass($name, static::class);
67
        }
68
69
        return new $stateClass($model);
70
    }
71
72
    /**
73
     * Create a state object based on a value (classname or name),
74
     * and optionally provide its constructor arguments.
75
     *
76
     * @param string $name
77
     * @param mixed ...$args
78
     *
79
     * @return \Spatie\ModelStates\State
80
     */
81
    public static function find(string $name, Model $model): State
82
    {
83
        return static::make($name, $model);
84
    }
85
86
    /**
87
     * Get all registered state classes.
88
     *
89
     * @return \Illuminate\Support\Collection|string[]|static[] A list of class names.
90
     */
91
    public static function all(): Collection
92
    {
93
        return collect(self::resolveStateMapping());
94
    }
95
96
    /**
97
     * The value that will be saved in the database.
98
     *
99
     * @return string
100
     */
101
    public static function getMorphClass(): string
102
    {
103
        return static::resolveStateName(static::class);
104
    }
105
106
    /**
107
     * The value that will be saved in the database.
108
     *
109
     * @return string
110
     */
111
    public function getValue(): string
112
    {
113
        return static::getMorphClass();
114
    }
115
116
    /**
117
     * Resolve the state class based on a value, for example a stored value in the database.
118
     *
119
     * @param string|\Spatie\ModelStates\State $state
120
     *
121
     * @return string
122
     */
123
    public static function resolveStateClass($state): ?string
124
    {
125
        if ($state === null) {
126
            return null;
127
        }
128
129
        if ($state instanceof State) {
130
            return get_class($state);
131
        }
132
133
        foreach (static::resolveStateMapping() as $stateClass) {
0 ignored issues
show
Bug introduced by
Since resolveStateMapping() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of resolveStateMapping() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
134
            if (! class_exists($stateClass)) {
135
                continue;
136
            }
137
138
            // Loose comparison is needed here in order to support non-string values,
139
            // Laravel casts their database value automatically to strings if we didn't specify the fields in `$casts`.
140
            if (($stateClass::$name ?? null) == $state) {
141
                return $stateClass;
142
            }
143
        }
144
145
        return $state;
146
    }
147
148
    /**
149
     * Resolve the name of the state, which is the value that will be saved in the database.
150
     *
151
     * Possible names are:
152
     *
153
     *    - The classname, is no explicit name is provided
154
     *    - A name provided in the state class as a public static property:
155
     *      `public static $name = 'dummy'`
156
     *
157
     * @param $state
158
     *
159
     * @return string|null
160
     */
161
    public static function resolveStateName($state): ?string
162
    {
163
        if ($state === null) {
164
            return null;
165
        }
166
167
        if ($state instanceof State) {
168
            $stateClass = get_class($state);
169
        } else {
170
            $stateClass = static::resolveStateClass($state);
171
        }
172
173
        if (class_exists($stateClass) && isset($stateClass::$name)) {
174
            return $stateClass::$name;
175
        }
176
177
        return $stateClass;
178
    }
179
180
    /**
181
     * Determine if the current state is one of an arbitrary number of other states.
182
     * This can be either a classname or a name.
183
     *
184
     * @param string|array ...$stateClasses
185
     *
186
     * @return bool
187
     */
188
    public function isOneOf(...$statesNames): bool
189
    {
190
        $statesNames = collect($statesNames)->flatten()->toArray();
191
192
        foreach ($statesNames as $statesName) {
193
            if ($this->equals($statesName)) {
194
                return true;
195
            }
196
        }
197
198
        return false;
199
    }
200
201
    /**
202
     * Determine if the current state equals another.
203
     * This can be either a classname or a name.
204
     *
205
     * @param string|\Spatie\ModelStates\State $state
206
     *
207
     * @return bool
208
     */
209
    public function equals($state): bool
210
    {
211
        return self::resolveStateClass($state)
212
            === self::resolveStateClass($this);
213
    }
214
215
    /**
216
     * Determine if the current state equals another.
217
     * This can be either a classname or a name.
218
     *
219
     * @param string|\Spatie\ModelStates\State $state
220
     *
221
     * @return bool
222
     */
223
    public function is($state): bool
224
    {
225
        return $this->equals($state);
226
    }
227
228
    public function __toString(): string
229
    {
230
        return static::getMorphClass();
231
    }
232
233
    /**
234
     * @param string|\Spatie\ModelStates\Transition $transitionClass
0 ignored issues
show
Documentation introduced by
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...
235
     * @param mixed ...$args
236
     *
237
     * @return \Illuminate\Database\Eloquent\Model
238
     */
239
    public function transition($transition, ...$args): Model
240
    {
241
        if (is_string($transition)) {
242
            $transition = new $transition($this->model, ...$args);
243
        }
244
245
        if (method_exists($transition, 'canTransition')) {
246
            if (! $transition->canTransition()) {
247
                throw CouldNotPerformTransition::notAllowed($this->model, $transition);
248
            }
249
        }
250
251
        $mutatedModel = app()->call([$transition, 'handle']);
252
253
        event(new StateChanged($this, $mutatedModel->state, $transition, $this->model));
254
255
        return $mutatedModel;
256
    }
257
258
    /**
259
     * @param string|\Spatie\ModelStates\State $state
260
     * @param mixed ...$args
261
     *
262
     * @return \Illuminate\Database\Eloquent\Model
263
     */
264
    public function transitionTo($state, ...$args): Model
265
    {
266
        if (! method_exists($this->model, 'resolveTransitionClass')) {
267
            throw InvalidConfig::resolveTransitionNotFound($this->model);
268
        }
269
270
        $transition = $this->model->resolveTransitionClass(
271
            static::resolveStateClass($this),
272
            static::resolveStateClass($state)
273
        );
274
275
        return $this->transition($transition, ...$args);
276
    }
277
278
    /**
279
     * Get the transitionable states from this state.
280
     *
281
     * @return array
282
     */
283
    public function transitionableStates($field = null): array
284
    {
285
        return $this->model->transitionableStates(get_class($this), $field);
286
    }
287
288
    /**
289
     * This method is used to find all available implementations of a given abstract state class.
290
     * Finding all implementations can be done in two ways:.
291
     *
292
     *    - The developer can define his own mapping directly in abstract state classes
293
     *      via the `protected $states = []` property
294
     *    - If no specific mapping was provided, the same directory where the abstract state class lives
295
     *      is scanned, and all concrete state classes extending the abstract state class will be provided.
296
     *
297
     * @return array
298
     */
299
    private static function resolveStateMapping(): array
300
    {
301
        if (isset(static::$states)) {
302
            return static::$states;
303
        }
304
305
        if (isset(self::$generatedMapping[static::class])) {
306
            return self::$generatedMapping[static::class];
307
        }
308
309
        $reflection = new ReflectionClass(static::class);
310
311
        ['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...
312
313
        $files = scandir($directory);
314
315
        unset($files[0], $files[1]);
316
317
        $namespace = $reflection->getNamespaceName();
318
319
        $resolvedStates = [];
320
321
        foreach ($files as $file) {
322
            ['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...
323
324
            $stateClass = $namespace . '\\' . $className;
325
326
            if (! is_subclass_of($stateClass, static::class)) {
327
                continue;
328
            }
329
330
            $resolvedStates[] = $stateClass;
331
        }
332
333
        self::$generatedMapping[static::class] = $resolvedStates;
334
335
        return self::$generatedMapping[static::class];
336
    }
337
338
    public function jsonSerialize()
339
    {
340
        return $this->getValue();
341
    }
342
}
343