Completed
Pull Request — master (#23)
by Travis
01:10
created

State::__toString()   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 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
     * This method is used to find all available implementations of a given abstract state class.
260
     * Finding all implementations can be done in two ways:.
261
     *
262
     *    - The developer can define his own mapping directly in abstract state classes
263
     *      via the `protected $states = []` property
264
     *    - If no specific mapping was provided, the same directory where the abstract state class lives
265
     *      is scanned, and all concrete state classes extending the abstract state class will be provided.
266
     *
267
     * @return array
268
     */
269
    private static function resolveStateMapping(): array
270
    {
271
        if (isset(static::$states)) {
272
            return static::$states;
273
        }
274
275
        if (isset(self::$generatedMapping[static::class])) {
276
            return self::$generatedMapping[static::class];
277
        }
278
279
        $reflection = new ReflectionClass(static::class);
280
281
        ['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...
282
283
        $files = scandir($directory);
284
285
        unset($files[0], $files[1]);
286
287
        $namespace = $reflection->getNamespaceName();
288
289
        $resolvedStates = [];
290
291
        foreach ($files as $file) {
292
            ['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...
293
294
            $stateClass = $namespace.'\\'.$className;
295
296
            if (! is_subclass_of($stateClass, static::class)) {
297
                continue;
298
            }
299
300
            $resolvedStates[] = $stateClass;
301
        }
302
303
        self::$generatedMapping[static::class] = $resolvedStates;
304
305
        return self::$generatedMapping[static::class];
306
    }
307
308
    public function jsonSerialize()
309
    {
310
        return $this->getValue();
311
    }
312
}
313