1 | <?php |
||||
2 | |||||
3 | namespace Spatie\ModelStates; |
||||
4 | |||||
5 | use Illuminate\Database\Eloquent\Builder; |
||||
6 | use Illuminate\Database\Eloquent\Model; |
||||
7 | use Illuminate\Support\Arr; |
||||
8 | use Illuminate\Support\Collection; |
||||
9 | use Spatie\ModelStates\Exceptions\CouldNotPerformTransition; |
||||
10 | use Spatie\ModelStates\Exceptions\InvalidConfig; |
||||
11 | |||||
12 | /** |
||||
13 | * @mixin \Illuminate\Database\Eloquent\Model |
||||
14 | * |
||||
15 | * @method static Builder whereState(string $field, string|string[] $states) |
||||
16 | * @method static Builder whereNotState(string $field, string|string[] $states) |
||||
17 | */ |
||||
18 | trait HasStates |
||||
19 | { |
||||
20 | /** @var \Spatie\ModelStates\StateConfig[]|null */ |
||||
21 | protected static $stateFields = null; |
||||
22 | |||||
23 | abstract protected function registerStates(): void; |
||||
24 | |||||
25 | public static function bootHasStates(): void |
||||
26 | { |
||||
27 | $serializeState = function (StateConfig $stateConfig) { |
||||
28 | return function (Model $model) use ($stateConfig) { |
||||
29 | $value = $model->getAttribute($stateConfig->field); |
||||
30 | |||||
31 | if ($value === null) { |
||||
32 | $value = $stateConfig->defaultStateClass; |
||||
33 | } |
||||
34 | |||||
35 | if ($value === null) { |
||||
36 | return; |
||||
37 | } |
||||
38 | |||||
39 | $stateClass = $stateConfig->stateClass::resolveStateClass($value); |
||||
40 | |||||
41 | if (! is_subclass_of($stateClass, $stateConfig->stateClass)) { |
||||
42 | throw InvalidConfig::fieldDoesNotExtendState( |
||||
43 | $stateConfig->field, |
||||
44 | $stateConfig->stateClass, |
||||
45 | $stateClass |
||||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
46 | ); |
||||
47 | } |
||||
48 | |||||
49 | $model->setAttribute( |
||||
50 | $stateConfig->field, |
||||
51 | State::resolveStateName($value) |
||||
52 | ); |
||||
53 | }; |
||||
54 | }; |
||||
55 | |||||
56 | $unserializeState = function (StateConfig $stateConfig) { |
||||
57 | return function (Model $model) use ($stateConfig) { |
||||
58 | $stateClass = $stateConfig->stateClass::resolveStateClass($model->getAttribute($stateConfig->field)); |
||||
59 | |||||
60 | $defaultState = $stateConfig->defaultStateClass |
||||
61 | ? new $stateConfig->defaultStateClass($model) |
||||
62 | : null; |
||||
63 | |||||
64 | $model->setAttribute( |
||||
65 | $stateConfig->field, |
||||
66 | class_exists($stateClass) |
||||
67 | ? new $stateClass($model) |
||||
68 | : $defaultState |
||||
69 | ); |
||||
70 | }; |
||||
71 | }; |
||||
72 | |||||
73 | foreach (self::getStateConfig() as $stateConfig) { |
||||
74 | static::retrieved($unserializeState($stateConfig)); |
||||
75 | static::created($unserializeState($stateConfig)); |
||||
76 | static::updated($unserializeState($stateConfig)); |
||||
77 | static::saved($unserializeState($stateConfig)); |
||||
78 | |||||
79 | static::updating($serializeState($stateConfig)); |
||||
80 | static::creating($serializeState($stateConfig)); |
||||
81 | static::saving($serializeState($stateConfig)); |
||||
82 | } |
||||
83 | } |
||||
84 | |||||
85 | public function initializeHasStates(): void |
||||
86 | { |
||||
87 | foreach (self::getStateConfig() as $stateConfig) { |
||||
88 | if (! $stateConfig->defaultStateClass) { |
||||
89 | continue; |
||||
90 | } |
||||
91 | |||||
92 | $this->{$stateConfig->field} = new $stateConfig->defaultStateClass($this); |
||||
93 | } |
||||
94 | } |
||||
95 | |||||
96 | public function scopeWhereState(Builder $builder, string $column, $states): Builder |
||||
97 | { |
||||
98 | $field = Arr::last(explode('.', $column)); |
||||
99 | |||||
100 | /** @var \Spatie\ModelStates\StateConfig|null $stateConfig */ |
||||
101 | $stateConfig = self::getStateConfig()[$field] ?? null; |
||||
102 | |||||
103 | if (! $stateConfig) { |
||||
104 | throw InvalidConfig::unknownState($field, $this); |
||||
105 | } |
||||
106 | |||||
107 | $abstractStateClass = $stateConfig->stateClass; |
||||
108 | |||||
109 | $stateNames = collect((array) $states)->map(function ($state) use ($abstractStateClass) { |
||||
110 | return $abstractStateClass::resolveStateName($state); |
||||
111 | }); |
||||
112 | |||||
113 | return $builder->whereIn($column ?? $column, $stateNames); |
||||
114 | } |
||||
115 | |||||
116 | public function scopeWhereNotState(Builder $builder, string $column, $states): Builder |
||||
117 | { |
||||
118 | $field = Arr::last(explode('.', $column)); |
||||
119 | |||||
120 | /** @var \Spatie\ModelStates\StateConfig|null $stateConfig */ |
||||
121 | $stateConfig = self::getStateConfig()[$field] ?? null; |
||||
122 | |||||
123 | if (! $stateConfig) { |
||||
124 | throw InvalidConfig::unknownState($field, $this); |
||||
125 | } |
||||
126 | |||||
127 | $stateNames = collect((array) $states)->map(function ($state) use ($stateConfig) { |
||||
128 | return $stateConfig->stateClass::resolveStateName($state); |
||||
129 | }); |
||||
130 | |||||
131 | return $builder->whereNotIn($column ?? $column, $stateNames); |
||||
132 | } |
||||
133 | |||||
134 | /** |
||||
135 | * @param \Spatie\ModelStates\State|string $state |
||||
136 | * @param string|null $field |
||||
137 | * |
||||
138 | * @return \Illuminate\Database\Eloquent\Model |
||||
139 | */ |
||||
140 | public function transitionTo($state, string $field = null) |
||||
141 | { |
||||
142 | $stateConfig = self::getStateConfig(); |
||||
143 | |||||
144 | if ($field === null && count($stateConfig) > 1) { |
||||
145 | throw CouldNotPerformTransition::couldNotResolveTransitionField($this); |
||||
0 ignored issues
–
show
$this of type Spatie\ModelStates\HasStates is incompatible with the type Illuminate\Database\Eloquent\Model expected by parameter $model of Spatie\ModelStates\Excep...esolveTransitionField() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
146 | } |
||||
147 | |||||
148 | $field = $field ?? reset($stateConfig)->field; |
||||
149 | |||||
150 | return $this->{$field}->transitionTo($state); |
||||
151 | } |
||||
152 | |||||
153 | public function transitionableStates(string $fromClass, ?string $field = null): array |
||||
154 | { |
||||
155 | $stateConfig = self::getStateConfig(); |
||||
156 | |||||
157 | if ($field === null && count($stateConfig) > 1) { |
||||
158 | throw InvalidConfig::fieldNotFound($fromClass, $this); |
||||
159 | } |
||||
160 | |||||
161 | $field = $field ?? reset($stateConfig)->field; |
||||
162 | |||||
163 | if (! array_key_exists($field, $stateConfig)) { |
||||
164 | throw InvalidConfig::unknownState($field, $this); |
||||
165 | } |
||||
166 | |||||
167 | return $stateConfig[$field]->transitionableStates($fromClass); |
||||
168 | } |
||||
169 | |||||
170 | /** |
||||
171 | * @param \Spatie\ModelStates\State|string $to |
||||
172 | * @param string|null $field |
||||
173 | * |
||||
174 | * @return bool |
||||
175 | */ |
||||
176 | public function canTransitionTo($to, ?string $field = null): bool |
||||
177 | { |
||||
178 | $statesConfig = self::getStateConfig(); |
||||
179 | |||||
180 | if ($field === null && count($statesConfig) > 1) { |
||||
181 | throw InvalidConfig::fieldNotFound(($to instanceof State) ? get_class($to) : $to, $this); |
||||
182 | } |
||||
183 | |||||
184 | $field = $field ?? reset($statesConfig)->field; |
||||
185 | |||||
186 | $stateConfig = $statesConfig[$field]; |
||||
187 | |||||
188 | try { |
||||
189 | $this->resolveTransitionClass( |
||||
190 | $stateConfig->stateClass::resolveStateClass($this->$field), |
||||
191 | $stateConfig->stateClass::resolveStateClass($to) |
||||
192 | ); |
||||
193 | } catch (CouldNotPerformTransition $exception) { |
||||
194 | return false; |
||||
195 | } |
||||
196 | |||||
197 | return true; |
||||
198 | } |
||||
199 | |||||
200 | /** |
||||
201 | * @param string $fromClass |
||||
202 | * @param string $toClass |
||||
203 | * |
||||
204 | * @return \Spatie\ModelStates\Transition|string|null |
||||
205 | */ |
||||
206 | public function resolveTransitionClass(string $fromClass, string $toClass) |
||||
207 | { |
||||
208 | foreach (static::getStateConfig() as $stateConfig) { |
||||
209 | $transitionClass = $stateConfig->resolveTransition($this, $fromClass, $toClass); |
||||
210 | |||||
211 | if ($transitionClass) { |
||||
212 | return $transitionClass; |
||||
213 | } |
||||
214 | } |
||||
215 | |||||
216 | throw CouldNotPerformTransition::notFound($fromClass, $toClass, $this); |
||||
217 | } |
||||
218 | |||||
219 | protected function addState(string $field, string $stateClass): StateConfig |
||||
220 | { |
||||
221 | $stateConfig = new StateConfig($field, $stateClass); |
||||
222 | |||||
223 | static::$stateFields[$stateConfig->field] = $stateConfig; |
||||
224 | |||||
225 | return $stateConfig; |
||||
226 | } |
||||
227 | |||||
228 | /** |
||||
229 | * @return \Spatie\ModelStates\StateConfig[] |
||||
230 | */ |
||||
231 | private static function getStateConfig(): array |
||||
232 | { |
||||
233 | if (static::$stateFields === null) { |
||||
234 | static::$stateFields = []; |
||||
235 | |||||
236 | (new static)->registerStates(); |
||||
237 | } |
||||
238 | |||||
239 | return static::$stateFields ?? []; |
||||
240 | } |
||||
241 | |||||
242 | public static function getStates(): Collection |
||||
243 | { |
||||
244 | return collect(static::getStateConfig()) |
||||
245 | ->map(function ($state) { |
||||
246 | return $state->stateClass::all(); |
||||
247 | }); |
||||
248 | } |
||||
249 | |||||
250 | public static function getStatesFor(string $column): Collection |
||||
251 | { |
||||
252 | return static::getStates()->get($column, new Collection); |
||||
253 | } |
||||
254 | |||||
255 | public static function getDefaultStates(): Collection |
||||
256 | { |
||||
257 | return collect(static::getStateConfig()) |
||||
258 | ->map(function ($state) { |
||||
259 | return $state->defaultStateClass; |
||||
260 | }); |
||||
261 | } |
||||
262 | |||||
263 | public static function getDefaultStateFor(string $column): string |
||||
264 | { |
||||
265 | return static::getDefaultStates()->get($column); |
||||
266 | } |
||||
267 | } |
||||
268 |