Completed
Push — master ( 1de2e3...928b36 )
by Brent
02:04 queued 40s
created

State::transitionableStates()   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 1
1
<?php
2
3
namespace Spatie\ModelStates;
4
5
use ReflectionClass;
6
use JsonSerializable;
7
use Illuminate\Support\Collection;
8
use Illuminate\Database\Eloquent\Model;
9
use Spatie\ModelStates\Events\StateChanged;
10
use Spatie\ModelStates\Exceptions\InvalidConfig;
11
use Spatie\ModelStates\Exceptions\CouldNotPerformTransition;
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) {
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...
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, is 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
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...
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
        event(new StateChanged($this, $mutatedModel->state, $transition, $this->model));
234
235
        return $mutatedModel;
236
    }
237
238
    /**
239
     * @param string|\Spatie\ModelStates\State $state
240
     * @param mixed ...$args
241
     *
242
     * @return \Illuminate\Database\Eloquent\Model
243
     */
244
    public function transitionTo($state, ...$args): Model
245
    {
246
        if (! method_exists($this->model, 'resolveTransitionClass')) {
247
            throw InvalidConfig::resolveTransitionNotFound($this->model);
248
        }
249
250
        $transition = $this->model->resolveTransitionClass(
251
            static::resolveStateClass($this),
252
            static::resolveStateClass($state)
253
        );
254
255
        return $this->transition($transition, ...$args);
256
    }
257
258
    /**
259
     * Get the transitionable states from this state.
260
     *
261
     * @return array
262
     */
263
    public function transitionableStates($field = null): array
264
    {
265
        return $this->model->transitionableStates(get_class($this), $field);
266
    }
267
268
    /**
269
     * This method is used to find all available implementations of a given abstract state class.
270
     * Finding all implementations can be done in two ways:.
271
     *
272
     *    - The developer can define his own mapping directly in abstract state classes
273
     *      via the `protected $states = []` property
274
     *    - If no specific mapping was provided, the same directory where the abstract state class lives
275
     *      is scanned, and all concrete state classes extending the abstract state class will be provided.
276
     *
277
     * @return array
278
     */
279
    private static function resolveStateMapping(): array
280
    {
281
        if (isset(static::$states)) {
282
            return static::$states;
283
        }
284
285
        if (isset(self::$generatedMapping[static::class])) {
286
            return self::$generatedMapping[static::class];
287
        }
288
289
        $reflection = new ReflectionClass(static::class);
290
291
        ['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...
292
293
        $files = scandir($directory);
294
295
        unset($files[0], $files[1]);
296
297
        $namespace = $reflection->getNamespaceName();
298
299
        $resolvedStates = [];
300
301
        foreach ($files as $file) {
302
            ['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...
303
304
            $stateClass = $namespace.'\\'.$className;
305
306
            if (! is_subclass_of($stateClass, static::class)) {
307
                continue;
308
            }
309
310
            $resolvedStates[] = $stateClass;
311
        }
312
313
        self::$generatedMapping[static::class] = $resolvedStates;
314
315
        return self::$generatedMapping[static::class];
316
    }
317
318
    public function jsonSerialize()
319
    {
320
        return $this->getValue();
321
    }
322
}
323