1 | <?php |
||||
2 | |||||
3 | namespace Spatie\ModelStates; |
||||
4 | |||||
5 | use Illuminate\Database\Eloquent\Model; |
||||
6 | use Illuminate\Support\Collection; |
||||
7 | use JsonSerializable; |
||||
8 | use ReflectionClass; |
||||
9 | use Spatie\ModelStates\Events\StateChanged; |
||||
10 | use Spatie\ModelStates\Exceptions\CouldNotPerformTransition; |
||||
11 | use Spatie\ModelStates\Exceptions\InvalidConfig; |
||||
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 \Illuminate\Database\Eloquent\Model $model |
||||
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 \Illuminate\Database\Eloquent\Model $model |
||||
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 (self::resolveStateMapping() as $stateClass) { |
||||
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 | $name = isset($stateClass::$name) ? (string) $stateClass::$name : null; |
||||
121 | |||||
122 | if ($name == $state) { |
||||
123 | return $stateClass; |
||||
124 | } |
||||
125 | } |
||||
126 | |||||
127 | return $state; |
||||
128 | } |
||||
129 | |||||
130 | /** |
||||
131 | * Resolve the name of the state, which is the value that will be saved in the database. |
||||
132 | * |
||||
133 | * Possible names are: |
||||
134 | * |
||||
135 | * - The classname, if no explicit name is provided |
||||
136 | * - A name provided in the state class as a public static property: |
||||
137 | * `public static $name = 'dummy'` |
||||
138 | * |
||||
139 | * @param string|\Spatie\ModelStates\State $state |
||||
140 | * |
||||
141 | * @return string|null |
||||
142 | */ |
||||
143 | public static function resolveStateName($state): ?string |
||||
144 | { |
||||
145 | if ($state === null) { |
||||
146 | return null; |
||||
147 | } |
||||
148 | |||||
149 | if ($state instanceof State) { |
||||
150 | $stateClass = get_class($state); |
||||
151 | } else { |
||||
152 | $stateClass = static::resolveStateClass($state); |
||||
153 | } |
||||
154 | |||||
155 | if (class_exists($stateClass) && isset($stateClass::$name)) { |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
156 | return $stateClass::$name; |
||||
157 | } |
||||
158 | |||||
159 | return $stateClass; |
||||
160 | } |
||||
161 | |||||
162 | /** |
||||
163 | * Determine if the current state is one of an arbitrary number of other states. |
||||
164 | * This can be either a classname or a name. |
||||
165 | * |
||||
166 | * @param string|array ...$stateClasses |
||||
167 | * |
||||
168 | * @return bool |
||||
169 | */ |
||||
170 | public function isOneOf(...$statesNames): bool |
||||
171 | { |
||||
172 | $statesNames = collect($statesNames)->flatten()->toArray(); |
||||
173 | |||||
174 | foreach ($statesNames as $statesName) { |
||||
175 | if ($this->equals($statesName)) { |
||||
176 | return true; |
||||
177 | } |
||||
178 | } |
||||
179 | |||||
180 | return false; |
||||
181 | } |
||||
182 | |||||
183 | /** |
||||
184 | * Determine if the current state equals another. |
||||
185 | * This can be either a classname or a name. |
||||
186 | * |
||||
187 | * @param string|\Spatie\ModelStates\State $state |
||||
188 | * |
||||
189 | * @return bool |
||||
190 | */ |
||||
191 | public function equals($state): bool |
||||
192 | { |
||||
193 | return self::resolveStateClass($state) |
||||
194 | === self::resolveStateClass($this); |
||||
195 | } |
||||
196 | |||||
197 | /** |
||||
198 | * Determine if the current state equals another. |
||||
199 | * This can be either a classname or a name. |
||||
200 | * |
||||
201 | * @param string|\Spatie\ModelStates\State $state |
||||
202 | * |
||||
203 | * @return bool |
||||
204 | */ |
||||
205 | public function is($state): bool |
||||
206 | { |
||||
207 | return $this->equals($state); |
||||
208 | } |
||||
209 | |||||
210 | public function __toString(): string |
||||
211 | { |
||||
212 | return static::getMorphClass(); |
||||
213 | } |
||||
214 | |||||
215 | /** |
||||
216 | * @param string|\Spatie\ModelStates\Transition $transition |
||||
217 | * @param mixed ...$args |
||||
218 | * |
||||
219 | * @return \Illuminate\Database\Eloquent\Model |
||||
220 | */ |
||||
221 | public function transition($transition, ...$args): Model |
||||
222 | { |
||||
223 | if (is_string($transition)) { |
||||
224 | $transition = new $transition($this->model, ...$args); |
||||
225 | } |
||||
226 | |||||
227 | if (method_exists($transition, 'canTransition')) { |
||||
228 | if (! $transition->canTransition()) { |
||||
229 | throw CouldNotPerformTransition::notAllowed($this->model, $transition); |
||||
230 | } |
||||
231 | } |
||||
232 | |||||
233 | $mutatedModel = app()->call([$transition, 'handle']); |
||||
234 | |||||
235 | /* |
||||
236 | * There's a bug with the `finalState` variable: |
||||
237 | * `$mutatedModel->state` |
||||
238 | * was used, but this is wrong because we cannot determine the model field within this state class. |
||||
239 | * Hence `state` is hardcoded, but that's wrong. |
||||
240 | * |
||||
241 | * @see https://github.com/spatie/laravel-model-states/issues/49 |
||||
242 | */ |
||||
243 | $finalState = $mutatedModel->state; |
||||
244 | |||||
245 | if (! $finalState instanceof State) { |
||||
246 | $finalState = null; |
||||
247 | } |
||||
248 | |||||
249 | event(new StateChanged($this, $finalState, $transition, $this->model)); |
||||
250 | |||||
251 | return $mutatedModel; |
||||
0 ignored issues
–
show
|
|||||
252 | } |
||||
253 | |||||
254 | /** |
||||
255 | * @param string|\Spatie\ModelStates\State $state |
||||
256 | * @param mixed ...$args |
||||
257 | * |
||||
258 | * @return \Illuminate\Database\Eloquent\Model |
||||
259 | */ |
||||
260 | public function transitionTo($state, ...$args): Model |
||||
261 | { |
||||
262 | if (! method_exists($this->model, 'resolveTransitionClass')) { |
||||
263 | throw InvalidConfig::resolveTransitionNotFound($this->model); |
||||
264 | } |
||||
265 | |||||
266 | $transition = $this->model->resolveTransitionClass( |
||||
267 | static::resolveStateClass($this), |
||||
268 | static::resolveStateClass($state) |
||||
269 | ); |
||||
270 | |||||
271 | return $this->transition($transition, ...$args); |
||||
0 ignored issues
–
show
It seems like
$transition can also be of type Illuminate\Database\Eloquent\Builder ; however, parameter $transition of Spatie\ModelStates\State::transition() does only seem to accept Spatie\ModelStates\Transition|string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
272 | } |
||||
273 | |||||
274 | /** |
||||
275 | * Get the transitionable states from this state. |
||||
276 | * |
||||
277 | * @param string|null $field |
||||
278 | * |
||||
279 | * @return array |
||||
280 | */ |
||||
281 | public function transitionableStates($field = null): array |
||||
282 | { |
||||
283 | return $this->model->transitionableStates(get_class($this), $field); |
||||
0 ignored issues
–
show
|
|||||
284 | } |
||||
285 | |||||
286 | /** |
||||
287 | * This method is used to find all available implementations of a given abstract state class. |
||||
288 | * Finding all implementations can be done in two ways:. |
||||
289 | * |
||||
290 | * - The developer can define his own mapping directly in abstract state classes |
||||
291 | * via the `protected $states = []` property |
||||
292 | * - If no specific mapping was provided, the same directory where the abstract state class lives |
||||
293 | * is scanned, and all concrete state classes extending the abstract state class will be provided. |
||||
294 | * |
||||
295 | * @return array |
||||
296 | */ |
||||
297 | private static function resolveStateMapping(): array |
||||
298 | { |
||||
299 | if (isset(static::$states)) { |
||||
300 | return static::$states; |
||||
301 | } |
||||
302 | |||||
303 | if (isset(self::$generatedMapping[static::class])) { |
||||
304 | return self::$generatedMapping[static::class]; |
||||
305 | } |
||||
306 | |||||
307 | $reflection = new ReflectionClass(static::class); |
||||
308 | |||||
309 | ['dirname' => $directory] = pathinfo($reflection->getFileName()); |
||||
310 | |||||
311 | $files = scandir($directory); |
||||
312 | |||||
313 | unset($files[0], $files[1]); |
||||
314 | |||||
315 | $namespace = $reflection->getNamespaceName(); |
||||
316 | |||||
317 | $resolvedStates = []; |
||||
318 | |||||
319 | foreach ($files as $file) { |
||||
320 | ['filename' => $className] = pathinfo($file); |
||||
321 | |||||
322 | $stateClass = $namespace.'\\'.$className; |
||||
323 | |||||
324 | if (! is_subclass_of($stateClass, static::class)) { |
||||
325 | continue; |
||||
326 | } |
||||
327 | |||||
328 | $resolvedStates[] = $stateClass; |
||||
329 | } |
||||
330 | |||||
331 | self::$generatedMapping[static::class] = $resolvedStates; |
||||
332 | |||||
333 | return self::$generatedMapping[static::class]; |
||||
334 | } |
||||
335 | |||||
336 | public function jsonSerialize() |
||||
337 | { |
||||
338 | return $this->getValue(); |
||||
339 | } |
||||
340 | } |
||||
341 |