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

State::getStateConfig()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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