Completed
Pull Request — master (#53)
by Anthony
01:17
created

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