Passed
Push — main ( b3cf5c...f07e9b )
by Yaroslav
18:34
created

Flexible::resolveForDisplay()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace NovaFlexibleContent;
4
5
use Laravel\Nova\Contracts\Downloadable;
6
use Laravel\Nova\Fields\Field;
7
use Laravel\Nova\Fields\SupportsDependentFields;
8
use Laravel\Nova\Http\Requests\NovaRequest;
9
use NovaFlexibleContent\Http\ScopedRequest;
10
use NovaFlexibleContent\Layouts\Collections\GroupsCollection;
11
use NovaFlexibleContent\Layouts\Collections\LayoutsCollection as LayoutsCollection;
12
use NovaFlexibleContent\Layouts\Layout;
13
use NovaFlexibleContent\Nova\Fields\TraitsForFlexible\HasGroupRemovingConfirmation;
14
use NovaFlexibleContent\Nova\Fields\TraitsForFlexible\HasGroupsLimits;
15
use NovaFlexibleContent\Nova\Fields\TraitsForFlexible\HasLayoutsMenu;
16
use NovaFlexibleContent\Nova\Fields\TraitsForFlexible\HasOriginalModel;
17
use NovaFlexibleContent\Nova\Fields\TraitsForFlexible\HasPreset;
18
use NovaFlexibleContent\Nova\Fields\TraitsForFlexible\HasResolver;
19
20
class Flexible extends Field implements Downloadable
21
{
22
    use SupportsDependentFields;
23
    use HasResolver;
24
    use HasLayoutsMenu;
25
    use HasGroupsLimits;
26
    use HasGroupRemovingConfirmation;
27
    use HasPreset;
28
    use HasOriginalModel;
0 ignored issues
show
Deprecated Code introduced by
The trait NovaFlexibleContent\Nova...exible\HasOriginalModel has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

28
    use /** @scrutinizer ignore-deprecated */ HasOriginalModel;

This trait has been deprecated. The supplier of the trait has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the trait will be removed and what other trait to use instead.

Loading history...
29
30
    /**
31
     * The field's component.
32
     *
33
     * @var string
34
     */
35
    public $component = 'flexible-content';
36
37
    /**
38
     * @inerhitDoc
39
     */
40
    public $showOnIndex = false;
41
42
    /**
43
     * The available layouts as collection.
44
     */
45
    protected LayoutsCollection $layouts;
46
47
    /**
48
     * The currently created layout groups.
49
     */
50
    protected GroupsCollection $groups;
51
52
    /**
53
     * All the validated attributes
54
     */
55
    protected static array $validatedKeys = [];
56
57
    /**
58
     * Create a fresh flexible field instance
59
     *
60
     * @param string $name
61
     * @param string|null $attribute
62
     * @param mixed|null $resolveCallback
63
     * @return void
64
     */
65 15
    public function __construct($name, $attribute = null, $resolveCallback = null)
66
    {
67 15
        $this->layouts = new LayoutsCollection();
68 15
        $this->groups  = new GroupsCollection();
69
70 15
        parent::__construct($name, $attribute, $resolveCallback);
71
72 15
        foreach (class_uses_recursive($this) as $trait) {
73
74 15
            if (method_exists($this, $method = 'initialize' . class_basename($trait))) {
75 15
                $this->{$method}();
76
            }
77
        }
78
    }
79
80
    /**
81
     * Get the field layouts.
82
     */
83 1
    public function layouts(): LayoutsCollection
84
    {
85 1
        return $this->layouts;
86
    }
87
88
    /**
89
     * Get the field groups.
90
     */
91 7
    public function groups(): GroupsCollection
92
    {
93 7
        return $this->groups;
94
    }
95
96
    /**
97
     * Register a new layout.
98
     *
99
     * @deprecated Please use alternative useLayout
100
     */
101
    public function addLayout(...$arguments): static
102
    {
103
        $count = count($arguments);
104
105
        if ($count > 1) {
106
            $layout = new Layout(...$arguments);
0 ignored issues
show
Bug introduced by
$arguments is expanded, but the parameter $title of NovaFlexibleContent\Layouts\Layout::__construct() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

106
            $layout = new Layout(/** @scrutinizer ignore-type */ ...$arguments);
Loading history...
107
        } else {
108
            $layout = $arguments[0];
109
        }
110
111
        return $this->useLayout($layout);
112
    }
113
114
    /**
115
     * Register a new layout.
116
     */
117 15
    public function useLayout(Layout|string $layout): static
118
    {
119 15
        if (!is_a($layout, Layout::class, true)) {
120
            return $this;
121
        }
122
123 15
        if (is_string($layout)) {
124 10
            $layout = new $layout();
125
        }
126
127 15
        $this->registerLayout($layout);
128
129 15
        return $this;
130
    }
131
132
    /**
133
     * Push a layout instance into the layouts collection.
134
     */
135 15
    protected function registerLayout(Layout $layout)
136
    {
137 15
        $this->layouts->push($layout);
138
139 15
        return $this->withMeta(['layouts' => $this->layouts]);
140
    }
141
142
    /**
143
     * Resolve the field's value.
144
     *
145
     * @param mixed $resource
146
     * @param string|null $attribute
147
     * @return void
148
     */
149 10
    public function resolve($resource, $attribute = null): void
150
    {
151 10
        $attribute = $attribute ?? $this->attribute;
152
153 10
        $this->registerOriginModel($resource);
0 ignored issues
show
Deprecated Code introduced by
The function NovaFlexibleContent\Flex...::registerOriginModel() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

153
        /** @scrutinizer ignore-deprecated */ $this->registerOriginModel($resource);
Loading history...
154
155 10
        $this->buildGroups($resource, $attribute);
156
157 10
        $this->value = $this->resolveGroups($this->groups);
158
    }
159
160
    /**
161
     * Resolve the field's value for display on index and detail views.
162
     *
163
     * @param mixed $resource
164
     * @param string|null $attribute
165
     * @return void
166
     */
167 4
    public function resolveForDisplay($resource, $attribute = null)
168
    {
169 4
        $attribute = $attribute ?? $this->attribute;
170
171 4
        $this->registerOriginModel($resource);
0 ignored issues
show
Deprecated Code introduced by
The function NovaFlexibleContent\Flex...::registerOriginModel() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

171
        /** @scrutinizer ignore-deprecated */ $this->registerOriginModel($resource);
Loading history...
172
173 4
        $this->buildGroups($resource, $attribute);
174
175 4
        $this->value = $this->resolveGroupsForDisplay($this->groups);
176
    }
177
178
    /**
179
     * Check showing on detail.
180
     *
181
     * @param NovaRequest $request
182
     * @param             $resource
183
     * @return bool
184
     */
185
    public function isShownOnDetail(NovaRequest $request, $resource): bool
186
    {
187
        $this->layouts = $this->layouts->each(function (Layout $layout) use ($request, $resource) {
188
            $layout->filterForDetail($request, $resource);
189
        });
190
191
        return parent::isShownOnDetail($request, $resource);
192
    }
193
194
    /**
195
     * Hydrate the given attribute on the model based on the incoming request.
196
     *
197
     * @param \Laravel\Nova\Http\Requests\NovaRequest $request
198
     * @param string $requestAttribute
199
     * @param object $model
200
     * @param string $attribute
201
     * @return null|\Closure
202
     */
203 4
    protected function fillAttribute(NovaRequest $request, $requestAttribute, $model, $attribute)
204
    {
205 4
        if (!$request->exists($requestAttribute)) {
206 2
            return;
207
        }
208
209 3
        $attribute = $attribute ?? $this->attribute;
210
211 3
        $this->registerOriginModel($model);
0 ignored issues
show
Deprecated Code introduced by
The function NovaFlexibleContent\Flex...::registerOriginModel() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

211
        /** @scrutinizer ignore-deprecated */ $this->registerOriginModel($model);
Loading history...
212
213 3
        $this->buildGroups($model, $attribute);
214
215 3
        $callbacks = GroupsCollection::make($this->syncAndFillGroups($request, $requestAttribute, $model));
0 ignored issues
show
Bug introduced by
$this->syncAndFillGroups...questAttribute, $model) of type array is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Illuminate\Support\Collection::make(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

215
        $callbacks = GroupsCollection::make(/** @scrutinizer ignore-type */ $this->syncAndFillGroups($request, $requestAttribute, $model));
Loading history...
216
217 3
        $this->reFillValue($model, $attribute);
218
219 3
        if ($callbacks->isEmpty()) {
220 3
            return;
221
        }
222
223
        return function () use ($callbacks) {
224
            $callbacks->each->__invoke();
225
        };
226
    }
227
228 4
    public function reFillValue($model, ?string $attribute = null): static
229
    {
230 4
        $attribute   = $attribute ?? $this->attribute;
231 4
        $this->value = $this->resolver->set($model, $attribute, $this->groups);
232
233 4
        return $this;
234
    }
235
236
    /**
237
     * Process an incoming POST Request
238
     */
239 3
    protected function syncAndFillGroups(NovaRequest $request, string $requestAttribute, $model): array
240
    {
241 3
        if (!($raw = $this->extractValueFromRequest($request, $requestAttribute))) {
242
            $this->fireRemoveCallbacks(GroupsCollection::make(), $request, $model);
243
            $this->groups = GroupsCollection::make();
244
245
            return [];
246
        }
247
248 3
        $callbacks = [];
249 3
        $newGroups = GroupsCollection::make($raw)->map(function ($item, $key) use ($request, &$callbacks) {
0 ignored issues
show
Bug introduced by
$raw of type array is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Illuminate\Support\Collection::make(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

249
        $newGroups = GroupsCollection::make(/** @scrutinizer ignore-type */ $raw)->map(function ($item, $key) use ($request, &$callbacks) {
Loading history...
250 3
            $layout     = $item['layout'];
251 3
            $key        = $item['key'];
252 3
            $attributes = $item['attributes'];
253
254 3
            $group = $this->findGroup($key) ?? $this->newGroup($layout, $key);
255
256 3
            $group->setCollapsed((bool)($item['collapsed'] ?? false));
0 ignored issues
show
Bug introduced by
The method setCollapsed() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

256
            $group->/** @scrutinizer ignore-call */ 
257
                    setCollapsed((bool)($item['collapsed'] ?? false));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
257 3
            $scope     = ScopedRequest::scopeFrom($request, $attributes, $key);
258 3
            $callbacks = array_merge($callbacks, $group->fillFromRequest($scope));
259
260 3
            return $group;
261 3
        })->filter();
262
263 3
        $this->fireRemoveCallbacks($newGroups, $request, $model);
264
265 3
        $this->groups = $newGroups;
266
267 3
        return $callbacks;
268
    }
269
270
    /**
271
     * Fire's the remove callbacks on the layouts
272
     *
273
     * @param $newGroups GroupsCollection This should be (all) the new groups to bne compared against to find the
274
     *                   removed groups
275
     */
276 3
    protected function fireRemoveCallbacks(GroupsCollection $newGroups, NovaRequest $request, $model): static
277
    {
278 3
        $newGroupKeys = $newGroups->map(function ($item) {
279 3
            return $item->inUseKey();
280 3
        });
281
282 3
        $this->groups()->filter(function ($item) use ($newGroupKeys) {
283
            // Return only removed groups.
284
            return !$newGroupKeys->contains($item->inUseKey());
285 3
        })->fireRemoveCallback($this, $request, $model);
286
287 3
        return $this;
288
    }
289
290
    /**
291
     * Find the flexible's value in given request.
292
     */
293 6
    protected function extractValueFromRequest(NovaRequest $request, string $attribute): ?array
294
    {
295 6
        $value = $request->input($attribute);
296
297 6
        if (!$value) {
298 2
            return null;
299
        }
300
301 5
        if (!is_array($value)) {
302
            throw new \Exception('Unable to parse incoming Flexible content, data should be an array.');
303
        }
304
305 5
        return $value;
306
    }
307
308
    /**
309
     * Resolve all contained groups and their fields.
310
     */
311 10
    protected function resolveGroups(GroupsCollection $groups): GroupsCollection
312
    {
313 10
        return $groups->map(function (Layout $group) {
314 4
            return $group->getResolved();
315 10
        });
316
    }
317
318
    /**
319
     * Resolve all contained groups and their fields for display on index and
320
     * detail views.
321
     */
322 4
    protected function resolveGroupsForDisplay(GroupsCollection $groups): GroupsCollection
323
    {
324 4
        return $groups->map(function ($group) {
325 4
            return $group->getResolvedForDisplay();
326 4
        });
327
    }
328
329
    /**
330
     * Define the field's actual layout groups (as "base models") based
331
     * on the field's current model & attribute
332
     *
333
     * @param mixed $resource
334
     * @param string $attribute
335
     * @return GroupsCollection
336
     */
337 13
    protected function buildGroups($resource, string $attribute): GroupsCollection
338
    {
339 13
        return $this->groups = $this->resolver->get($resource, $attribute, $this->layouts);
340
    }
341
342 3
    public function findGroup(string $groupKey): ?Layout
343
    {
344 3
        return $this->groups->first(fn (Layout $group) => $group->isUseKey($groupKey));
345
    }
346
347 4
    public function findGroupRecursive(string $groupKey): ?Layout
348
    {
349
        /** @var Layout $group */
350 4
        foreach ($this->groups as $group) {
351 4
            if ($group->isUseKey($groupKey)) {
352 3
                return $group;
353
            }
354 4
            if ($foundSubsequenceGroup = $group->findFlexibleGroupRecursive($groupKey)) {
355 2
                return $foundSubsequenceGroup;
356
            }
357
        }
358
359 2
        return null;
360
    }
361
362
    /**
363
     * @return bool - true if group found and false if not found.
364
     */
365 1
    public function setAttributeRecursive(string $groupKey, string $fieldKey, mixed $newValue = null): bool
366
    {
367 1
        $isUpdated = false;
368
369 1
        $this->groups
370 1
            ->each(function (Layout $group) use ($groupKey, $fieldKey, $newValue, &$isUpdated) {
371 1
                if ($group->isUseKey($groupKey)) {
372
                    $group->setAttribute($fieldKey, $newValue);
373
                    $isUpdated = true;
374
375
                    // Break loop
376
                    return false;
377
                }
378
379 1
                if ($group->findGroupRecursiveAndSetAttribute($groupKey, $fieldKey, $newValue)) {
380 1
                    $isUpdated = true;
381
382
                    // Break loop
383 1
                    return false;
384
                }
385 1
            });
386
387 1
        return $isUpdated;
388
    }
389
390
    /**
391
     * Create a new group based on its key and layout.
392
     */
393 5
    protected function newGroup(string $layout, string $key): ?Layout
394
    {
395 5
        return $this->layouts->find($layout)?->duplicate($key);
396
    }
397
398
    /**
399
     * Get the creation rules for this field & its contained fields.
400
     *
401
     * @param \Laravel\Nova\Http\Requests\NovaRequest $request
402
     * @return array
403
     */
404
    public function getCreationRules(NovaRequest $request): array
405
    {
406
        return array_merge_recursive(
407
            parent::getCreationRules($request),
408
            $this->getFlexibleRules($request, 'creation')
409
        );
410
    }
411
412
    /**
413
     * Get the update rules for this field & its contained fields.
414
     *
415
     * @param \Laravel\Nova\Http\Requests\NovaRequest $request
416
     * @return array
417
     * @throws \Exception
418
     */
419 6
    public function getUpdateRules(NovaRequest $request): array
420
    {
421 6
        return array_merge_recursive(
422 6
            parent::getUpdateRules($request),
423 6
            $this->getFlexibleRules($request, 'update')
424 6
        );
425
    }
426
427
    /**
428
     * Retrieve contained fields rules and assign them to nested array attributes.
429
     *
430
     * @param NovaRequest $request
431
     * @param string|null $type
432
     * @return array
433
     * @throws \Exception
434
     */
435 6
    protected function getFlexibleRules(NovaRequest $request, ?string $type = null): array
436
    {
437
438 6
        if (!($value = $this->extractValueFromRequest($request, $this->attribute))) {
439 2
            return [];
440
        }
441
442 5
        $rules = $this->generateRules($request, $value, $type);
443
444 5
        if (!is_a($request, ScopedRequest::class)) {
445
            // We're not in a nested flexible, meaning we're
446
            // assuming the field is located at the root of
447
            // the model's attributes. Therefore, we should now
448
            // register all the collected validation rules for later
449
            // reference (see Http\TransformsFlexibleErrors).
450 5
            static::registerValidationKeys($rules);
451
452
            // Then, transform the rules into an array that's actually
453
            // usable by Laravel's Validator.
454 5
            $rules = array_map(fn ($field) => $field['rules'], $rules);
455
        }
456
457 5
        return $rules;
458
    }
459
460
    /**
461
     * Format all contained fields rules and return them.
462
     *
463
     * @param NovaRequest $request
464
     * @param array $value
465
     * @param string|null $type
466
     * @return array
467
     */
468 5
    protected function generateRules(NovaRequest $request, array $value = [], ?string $type = null): array
469
    {
470 5
        return GroupsCollection::make($value)->map(function ($item, $key) use ($request, $type) {
0 ignored issues
show
Bug introduced by
$value of type array is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Illuminate\Support\Collection::make(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

470
        return GroupsCollection::make(/** @scrutinizer ignore-type */ $value)->map(function ($item, $key) use ($request, $type) {
Loading history...
471 5
            $group = $this->newGroup($item['layout'], $item['key']);
472
473 5
            if (!$group) {
474
                return [];
475
            }
476
477 5
            $scope = ScopedRequest::scopeFrom($request, $item['attributes'], $item['key']);
478
479 5
            return $group->generateRules($scope, "{$this->attribute}.{$key}", $type);
480 5
        })
481 5
            ->collapse()
482 5
            ->all();
483
    }
484
485
    /**
486
     * Add validation keys to the valdiatedKeys register, which will be
487
     * used for transforming validation errors later in the request cycle.
488
     *
489
     * @param array $rules
490
     * @return void
491
     */
492 5
    protected static function registerValidationKeys(array $rules): void
493
    {
494 5
        static::$validatedKeys = array_merge(
495 5
            static::$validatedKeys,
496 5
            array_filter(array_map(fn ($field) => $field['attribute']??null, $rules))
497 5
        );
498
    }
499
500
    /**
501
     * Return a previously registered validation key
502
     *
503
     * @param string $key
504
     * @return null|\NovaFlexibleContent\Http\FlexibleAttribute
505
     */
506 1
    public static function getValidationKey(string $key): ?Http\FlexibleAttribute
507
    {
508 1
        return static::$validatedKeys[$key] ?? null;
509
    }
510
511
}
512