Completed
Push — master ( 01a20f...120181 )
by Brent
07:52
created

State::jsonSerialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Spatie\ModelStates;
4
5
use JsonSerializable;
6
use ReflectionClass;
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
            if (($stateClass::$name ?? null) === $state) {
119
                return $stateClass;
120
            }
121
        }
122
123
        return $state;
124
    }
125
126
    /**
127
     * Resolve the name of the state, which is the value that will be saved in the database.
128
     *
129
     * Possible names are:
130
     *
131
     *    - The classname, is no explicit name is provided
132
     *    - A name provided in the state class as a public static property:
133
     *      `public static $name = 'dummy'`
134
     *
135
     * @param $state
136
     *
137
     * @return string|null
138
     */
139
    public static function resolveStateName($state): ?string
140
    {
141
        if ($state === null) {
142
            return null;
143
        }
144
145
        if ($state instanceof State) {
146
            $stateClass = get_class($state);
147
        } else {
148
            $stateClass = static::resolveStateClass($state);
149
        }
150
151
        if (class_exists($stateClass) && isset($stateClass::$name)) {
152
            return $stateClass::$name;
153
        }
154
155
        return $stateClass;
156
    }
157
158
    /**
159
     * Determine if the current state is one of an arbitrary number of other states.
160
     * This can be either a classname or a name.
161
     *
162
     * @param string|array ...$stateClasses
163
     *
164
     * @return bool
165
     */
166
    public function isOneOf(...$statesNames): bool
167
    {
168
        $statesNames = collect($statesNames)->flatten()->toArray();
169
170
        foreach ($statesNames as $statesName) {
171
            if ($this->equals($statesName)) {
172
                return true;
173
            }
174
        }
175
176
        return false;
177
    }
178
179
    /**
180
     * Determine if the current state equals another.
181
     * This can be either a classname or a name.
182
     *
183
     * @param string|\Spatie\ModelStates\State $state
184
     *
185
     * @return bool
186
     */
187
    public function equals($state): bool
188
    {
189
        return self::resolveStateClass($state)
190
            === self::resolveStateClass($this);
191
    }
192
193
    /**
194
     * Determine if the current state equals another.
195
     * This can be either a classname or a name.
196
     *
197
     * @param string|\Spatie\ModelStates\State $state
198
     *
199
     * @return bool
200
     */
201
    public function is($state): bool
202
    {
203
        return $this->equals($state);
204
    }
205
206
    public function __toString(): string
207
    {
208
        return static::getMorphClass();
209
    }
210
211
    /**
212
     * @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...
213
     * @param mixed ...$args
214
     *
215
     * @return \Illuminate\Database\Eloquent\Model
216
     */
217
    public function transition($transition, ...$args): Model
218
    {
219
        if (is_string($transition)) {
220
            $transition = new $transition($this->model, ...$args);
221
        }
222
223
        if (method_exists($transition, 'canTransition')) {
224
            if (! $transition->canTransition()) {
225
                throw CouldNotPerformTransition::notAllowed($this->model, $transition);
226
            }
227
        }
228
229
        $mutatedModel = app()->call([$transition, 'handle']);
230
231
        event(new StateChanged($this, $mutatedModel->state, $transition, $this->model));
232
233
        return $mutatedModel;
234
    }
235
236
    /**
237
     * @param string|\Spatie\ModelStates\State $state
238
     * @param mixed ...$args
239
     *
240
     * @return \Illuminate\Database\Eloquent\Model
241
     */
242
    public function transitionTo($state, ...$args): Model
243
    {
244
        if (! method_exists($this->model, 'resolveTransitionClass')) {
245
            throw InvalidConfig::resolveTransitionNotFound($this->model);
246
        }
247
248
        $transition = $this->model->resolveTransitionClass(
249
            static::resolveStateClass($this),
250
            static::resolveStateClass($state)
251
        );
252
253
        return $this->transition($transition, ...$args);
254
    }
255
256
    /**
257
     * This method is used to find all available implementations of a given abstract state class.
258
     * Finding all implementations can be done in two ways:.
259
     *
260
     *    - The developer can define his own mapping directly in abstract state classes
261
     *      via the `protected $states = []` property
262
     *    - If no specific mapping was provided, the same directory where the abstract state class lives
263
     *      is scanned, and all concrete state classes extending the abstract state class will be provided.
264
     *
265
     * @return array
266
     */
267
    private static function resolveStateMapping(): array
268
    {
269
        if (isset(static::$states)) {
270
            return static::$states;
271
        }
272
273
        if (isset(self::$generatedMapping[static::class])) {
274
            return self::$generatedMapping[static::class];
275
        }
276
277
        $reflection = new ReflectionClass(static::class);
278
279
        ['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...
280
281
        $files = scandir($directory);
282
283
        unset($files[0], $files[1]);
284
285
        $namespace = $reflection->getNamespaceName();
286
287
        $resolvedStates = [];
288
289
        foreach ($files as $file) {
290
            ['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...
291
292
            $stateClass = $namespace.'\\'.$className;
293
294
            if (! is_subclass_of($stateClass, static::class)) {
295
                continue;
296
            }
297
298
            $resolvedStates[] = $stateClass;
299
        }
300
301
        self::$generatedMapping[static::class] = $resolvedStates;
302
303
        return self::$generatedMapping[static::class];
304
    }
305
306
    public function jsonSerialize() {
307
        return $this->getValue();
308
    }
309
}
310