1 | <?php |
||
2 | |||
3 | namespace Bakery\Eloquent; |
||
4 | |||
5 | use Bakery\Utils\Utils; |
||
6 | use Bakery\Fields\Field; |
||
7 | use Illuminate\Support\Str; |
||
8 | use Bakery\Fields\EloquentField; |
||
9 | use Bakery\Support\TypeRegistry; |
||
10 | use Illuminate\Support\Collection; |
||
11 | use Illuminate\Database\Eloquent\Model; |
||
12 | use Illuminate\Database\Eloquent\Builder; |
||
13 | use Bakery\Eloquent\Concerns\Authorizable; |
||
14 | use Bakery\Eloquent\Concerns\MutatesModel; |
||
15 | |||
16 | abstract class ModelSchema |
||
17 | { |
||
18 | use Authorizable; |
||
19 | use MutatesModel; |
||
20 | |||
21 | /** |
||
22 | * @var \Bakery\Support\Schema |
||
23 | */ |
||
24 | protected $schema; |
||
25 | |||
26 | /** |
||
27 | * @var \Bakery\Support\TypeRegistry |
||
28 | */ |
||
29 | protected $registry; |
||
30 | |||
31 | /** |
||
32 | * @var \Illuminate\Contracts\Auth\Access\Gate |
||
33 | */ |
||
34 | protected $gate; |
||
35 | |||
36 | /** |
||
37 | * @var string |
||
38 | */ |
||
39 | protected $model; |
||
40 | |||
41 | /** |
||
42 | * @var Model |
||
43 | */ |
||
44 | protected $instance; |
||
45 | |||
46 | /** |
||
47 | * Indicates if the model can be mutated. |
||
48 | * Setting this to false will make sure no mutations are generated for that model. |
||
49 | * |
||
50 | * @var bool |
||
51 | */ |
||
52 | protected $mutable = true; |
||
53 | |||
54 | /** |
||
55 | * Indicates if the model can be indexed. |
||
56 | * |
||
57 | * @var bool |
||
58 | */ |
||
59 | protected $indexable = true; |
||
60 | |||
61 | /** |
||
62 | * ModelSchema constructor. |
||
63 | * |
||
64 | * @param \Bakery\Support\TypeRegistry $registry |
||
65 | * @param \Illuminate\Database\Eloquent\Model|null $instance |
||
66 | */ |
||
67 | public function __construct(TypeRegistry $registry, Model $instance = null) |
||
68 | { |
||
69 | $this->registry = $registry; |
||
70 | |||
71 | if ($instance) { |
||
72 | $this->instance = $instance; |
||
73 | } else { |
||
74 | $model = $this->model(); |
||
75 | |||
76 | Utils::invariant( |
||
77 | is_subclass_of($model, Model::class), |
||
78 | 'Defined model on '.class_basename($this).' is not a subclass of '.Model::class |
||
79 | ); |
||
80 | |||
81 | $this->instance = resolve($model); |
||
0 ignored issues
–
show
|
|||
82 | } |
||
83 | } |
||
84 | |||
85 | /** |
||
86 | * Define the eloquent model of the model schema. |
||
87 | * |
||
88 | * @return string |
||
89 | */ |
||
90 | protected function model() |
||
91 | { |
||
92 | return $this->model; |
||
93 | } |
||
94 | |||
95 | /** |
||
96 | * Get the fields of the model. |
||
97 | * |
||
98 | * @return array |
||
99 | */ |
||
100 | public function fields(): array |
||
101 | { |
||
102 | return []; |
||
103 | } |
||
104 | |||
105 | /** |
||
106 | * Get the relation fields of the model. |
||
107 | * |
||
108 | * @return array |
||
109 | */ |
||
110 | public function relations(): array |
||
111 | { |
||
112 | return []; |
||
113 | } |
||
114 | |||
115 | /** |
||
116 | * Get the class of the model. |
||
117 | * |
||
118 | * @return string |
||
119 | */ |
||
120 | public function getModelClass(): string |
||
121 | { |
||
122 | return $this->model; |
||
123 | } |
||
124 | |||
125 | /** |
||
126 | * Get the eloquent model of the model schema. |
||
127 | * |
||
128 | * @return Model |
||
129 | */ |
||
130 | public function getModel(): Model |
||
131 | { |
||
132 | return $this->instance; |
||
133 | } |
||
134 | |||
135 | /** |
||
136 | * Returns if the schema is mutable. |
||
137 | * |
||
138 | * @return bool |
||
139 | */ |
||
140 | public function isMutable() |
||
141 | { |
||
142 | if ($this->getFillableFields()->merge($this->getFillableRelationFields())->isEmpty()) { |
||
143 | return false; |
||
144 | } |
||
145 | |||
146 | return $this->mutable; |
||
147 | } |
||
148 | |||
149 | /** |
||
150 | * Determine if the model is indexable. |
||
151 | * |
||
152 | * @return bool |
||
153 | */ |
||
154 | public function isIndexable() |
||
155 | { |
||
156 | return $this->indexable; |
||
157 | } |
||
158 | |||
159 | /** |
||
160 | * Returns if the schema is sortable. |
||
161 | */ |
||
162 | public function isSortable(): bool |
||
163 | { |
||
164 | return $this->getSortableFields()->isNotEmpty(); |
||
165 | } |
||
166 | |||
167 | /** |
||
168 | * Returns if the schema is searchable. |
||
169 | * |
||
170 | * @return bool |
||
171 | */ |
||
172 | public function isSearchable() |
||
173 | { |
||
174 | return $this->getSearchableFields()->merge($this->getSearchableRelationFields())->isNotEmpty(); |
||
175 | } |
||
176 | |||
177 | /** |
||
178 | * Return the typename of the model schema. |
||
179 | * |
||
180 | * @return string |
||
181 | */ |
||
182 | public function getTypename(): string |
||
183 | { |
||
184 | return Utils::typename($this->getModel()); |
||
185 | } |
||
186 | |||
187 | /** |
||
188 | * @alias getTypename() |
||
189 | * @return string |
||
190 | */ |
||
191 | public function typename(): string |
||
192 | { |
||
193 | return $this->getTypename(); |
||
194 | } |
||
195 | |||
196 | /** |
||
197 | * Get the key (ID) field. |
||
198 | * |
||
199 | * @return array |
||
200 | */ |
||
201 | protected function getKeyField(): array |
||
202 | { |
||
203 | $key = $this->instance->getKeyName(); |
||
204 | |||
205 | if (! $key) { |
||
206 | return []; |
||
207 | } |
||
208 | |||
209 | $field = $this->registry->field($this->registry->ID()) |
||
210 | ->accessor($key) |
||
211 | ->fillable(false) |
||
212 | ->unique(); |
||
213 | |||
214 | return [$key => $field]; |
||
215 | } |
||
216 | |||
217 | /** |
||
218 | * Get all the readable fields. |
||
219 | * |
||
220 | * @return \Illuminate\Support\Collection |
||
221 | */ |
||
222 | public function getFields(): Collection |
||
223 | { |
||
224 | return collect($this->getKeyField())->merge($this->fields())->map(function (Field $field, string $key) { |
||
225 | if (! $field->getAccessor()) { |
||
226 | $field->accessor($key); |
||
227 | } |
||
228 | |||
229 | return $field; |
||
230 | }); |
||
231 | } |
||
232 | |||
233 | /** |
||
234 | * Get the fields that can be filled. |
||
235 | * |
||
236 | * This excludes the ID field and other fields that are guarded from |
||
237 | * mass assignment exceptions. |
||
238 | * |
||
239 | * @return \Illuminate\Support\Collection |
||
240 | */ |
||
241 | public function getFillableFields(): Collection |
||
242 | { |
||
243 | return $this->getFields()->filter(function (Field $field) { |
||
244 | return $field->isFillable(); |
||
245 | }); |
||
246 | } |
||
247 | |||
248 | /** |
||
249 | * Get the fields than can be sorted. |
||
250 | */ |
||
251 | public function getSortableFields(): Collection |
||
252 | { |
||
253 | return $this->getFields()->filter(function (Field $field) { |
||
254 | return $field->isSortable(); |
||
255 | }); |
||
256 | } |
||
257 | |||
258 | /** |
||
259 | * The fields that can be used in fulltext search. |
||
260 | * |
||
261 | * @return \Illuminate\Support\Collection |
||
262 | */ |
||
263 | public function getSearchableFields(): Collection |
||
264 | { |
||
265 | return $this->getFields()->filter(function (Field $field) { |
||
266 | return $field->isSearchable(); |
||
267 | }); |
||
268 | } |
||
269 | |||
270 | /** |
||
271 | * The relation fields that can be used in fulltext search. |
||
272 | * |
||
273 | * @return \Illuminate\Support\Collection |
||
274 | */ |
||
275 | public function getSearchableRelationFields(): Collection |
||
276 | { |
||
277 | return $this->getRelationFields()->filter(function (Field $field) { |
||
278 | return $field->isSearchable(); |
||
279 | }); |
||
280 | } |
||
281 | |||
282 | /** |
||
283 | * The fields that can be used to look up this model. |
||
284 | * |
||
285 | * @return \Illuminate\Support\Collection |
||
286 | */ |
||
287 | public function getLookupFields(): Collection |
||
288 | { |
||
289 | $fields = collect($this->getFields()) |
||
290 | ->filter(function (Field $field) { |
||
291 | return $field->isUnique(); |
||
292 | }); |
||
293 | |||
294 | $relations = collect($this->getRelationFields()) |
||
295 | ->filter(function ($field) { |
||
296 | return $field instanceof EloquentField; |
||
297 | }) |
||
298 | ->map(function (EloquentField $field) { |
||
299 | $lookupTypeName = $field->getName().'LookupType'; |
||
300 | |||
301 | return $this->registry->field($lookupTypeName); |
||
302 | }); |
||
303 | |||
304 | return collect() |
||
305 | ->merge($fields) |
||
306 | ->merge($relations) |
||
307 | ->map(function (Field $field) { |
||
308 | return $field->nullable(); |
||
309 | }); |
||
310 | } |
||
311 | |||
312 | /** |
||
313 | * Get the lookup types. |
||
314 | * |
||
315 | * @return \Illuminate\Support\Collection |
||
316 | */ |
||
317 | public function getLookupTypes(): Collection |
||
318 | { |
||
319 | return $this->getLookupFields()->map(function (Field $field) { |
||
320 | return $field->getType(); |
||
321 | }); |
||
322 | } |
||
323 | |||
324 | /** |
||
325 | * Get the relational fields. |
||
326 | * |
||
327 | * @return \Illuminate\Support\Collection |
||
328 | */ |
||
329 | public function getRelationFields(): Collection |
||
330 | { |
||
331 | return collect($this->relations())->map(function (Field $field, string $key) { |
||
332 | if (! $field->getAccessor()) { |
||
333 | $field->accessor($key); |
||
334 | } |
||
335 | |||
336 | if (! $field->getWith()) { |
||
337 | $field->with($field->getAccessor()); |
||
338 | } |
||
339 | |||
340 | return $field; |
||
341 | }); |
||
342 | } |
||
343 | |||
344 | /** |
||
345 | * Get the fillable relational fields. |
||
346 | * |
||
347 | * @return \Illuminate\Support\Collection |
||
348 | */ |
||
349 | public function getFillableRelationFields(): Collection |
||
350 | { |
||
351 | return $this->getRelationFields()->filter(function (Field $field) { |
||
352 | return $field->isFillable(); |
||
353 | }); |
||
354 | } |
||
355 | |||
356 | /** |
||
357 | * Get the Eloquent relations of the model. |
||
358 | * This will only return relations that are defined in the model schema. |
||
359 | * |
||
360 | * @return \Illuminate\Support\Collection |
||
361 | */ |
||
362 | public function getRelations(): Collection |
||
363 | { |
||
364 | return collect($this->relations())->map(function (Field $field, string $key) { |
||
365 | if (! $field->getAccessor()) { |
||
366 | $field->accessor($key); |
||
367 | } |
||
368 | |||
369 | return $field; |
||
370 | })->map(function (Field $field) { |
||
371 | if ($field instanceof EloquentField) { |
||
372 | return $field->getRelation($this->getModel()); |
||
373 | } |
||
374 | |||
375 | $accessor = $field->getAccessor(); |
||
376 | |||
377 | Utils::invariant( |
||
378 | method_exists($this->getModel(), $accessor), |
||
379 | 'Relation "'.$accessor.'" is not defined on "'.get_class($this->getModel()).'".' |
||
380 | ); |
||
381 | |||
382 | return $this->getModel()->{$accessor}(); |
||
383 | }); |
||
384 | } |
||
385 | |||
386 | /** |
||
387 | * Return a field with a defined. |
||
388 | * |
||
389 | * @param string $key |
||
390 | * @return Field|null |
||
391 | */ |
||
392 | public function getFieldByKey(string $key): ?Field |
||
393 | { |
||
394 | return $this->getFields()->merge($this->getRelationFields()) |
||
395 | ->first(function (Field $field, $fieldKey) use ($key) { |
||
396 | return $fieldKey === $key; |
||
397 | }); |
||
398 | } |
||
399 | |||
400 | /** |
||
401 | * Get the connections of the resource. |
||
402 | * |
||
403 | * @return \Illuminate\Support\Collection |
||
404 | */ |
||
405 | public function getConnections(): Collection |
||
406 | { |
||
407 | return collect($this->getRelationFields())->map(function (Field $field, $key) { |
||
408 | return $field->isList() ? Str::singular($key).'Ids' : $key.'Id'; |
||
409 | }); |
||
410 | } |
||
411 | |||
412 | /** |
||
413 | * Get the instance of the model schema. |
||
414 | * |
||
415 | * @return \Illuminate\Database\Eloquent\Model |
||
416 | */ |
||
417 | public function getInstance(): Model |
||
418 | { |
||
419 | return $this->instance; |
||
420 | } |
||
421 | |||
422 | /** |
||
423 | * Get the registry of the model schema. |
||
424 | * |
||
425 | * @return \Bakery\Support\TypeRegistry |
||
426 | */ |
||
427 | public function getRegistry(): TypeRegistry |
||
428 | { |
||
429 | return $this->registry; |
||
430 | } |
||
431 | |||
432 | /** |
||
433 | * Boot the query builder on the underlying model. |
||
434 | * |
||
435 | * @return \Illuminate\Database\Eloquent\Builder |
||
436 | */ |
||
437 | public function getQuery(): Builder |
||
438 | { |
||
439 | $model = $this->getModel(); |
||
440 | |||
441 | return $this->scopeQuery($model->newQuery()); |
||
442 | } |
||
443 | |||
444 | /** |
||
445 | * Scope the query on the model schema. This method can be overridden to always |
||
446 | * scope the query when executing queries/mutations on this schema. |
||
447 | * |
||
448 | * Note that this does not work for relations, in these cases you |
||
449 | * should consider using Laravel's global scopes. |
||
450 | * |
||
451 | * @param \Illuminate\Database\Eloquent\Builder $query |
||
452 | * @return \Illuminate\Database\Eloquent\Builder |
||
453 | */ |
||
454 | protected function scopeQuery(Builder $query): Builder |
||
455 | { |
||
456 | return $query; |
||
457 | } |
||
458 | } |
||
459 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.