Test Failed
Push — main ( 706477...92dbda )
by Yaroslav
03:43
created

Flexible::setAttributeRecursive()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 11
nc 1
nop 3
dl 0
loc 23
ccs 0
cts 13
cp 0
crap 12
rs 9.9
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 7
    public function __construct($name, $attribute = null, $resolveCallback = null)
66
    {
67 7
        $this->layouts = new LayoutsCollection();
68 7
        $this->groups  = new GroupsCollection();
69
70 7
        parent::__construct($name, $attribute, $resolveCallback);
71
72 7
        foreach (class_uses_recursive($this) as $trait) {
73
74 7
            if (method_exists($this, $method = 'initialize' . class_basename($trait))) {
75 7
                $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 4
    public function groups(): GroupsCollection
92
    {
93 4
        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 7
    public function useLayout(Layout|string $layout): static
118
    {
119 7
        if (!is_a($layout, Layout::class, true)) {
120
            return $this;
121
        }
122
123 7
        if (is_string($layout)) {
124 7
            $layout = new $layout();
125
        }
126
127 7
        $this->registerLayout($layout);
128
129 7
        return $this;
130
    }
131
132
    /**
133
     * Push a layout instance into the layouts collection.
134
     */
135 7
    protected function registerLayout(Layout $layout)
136
    {
137 7
        $this->layouts->push($layout);
138
139 7
        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 1
    public function resolve($resource, $attribute = null): void
150
    {
151 1
        $attribute = $attribute ?? $this->attribute;
152
153 1
        $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 1
        $this->buildGroups($resource, $attribute);
156
157 1
        $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 3
    public function resolveForDisplay($resource, $attribute = null)
168
    {
169 3
        $attribute = $attribute ?? $this->attribute;
170
171 3
        $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 3
        $this->buildGroups($resource, $attribute);
174
175 3
        $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
    protected function fillAttribute(NovaRequest $request, $requestAttribute, $model, $attribute)
204
    {
205
        if (!$request->exists($requestAttribute)) {
206
            return;
207
        }
208
209
        $attribute = $attribute ?? $this->attribute;
210
211
        $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
        $this->buildGroups($model, $attribute);
214
215
        $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
        $this->reFillValue($model, $attribute);
218
219
        if ($callbacks->isEmpty()) {
220
            return;
221
        }
222
223
        return function () use ($callbacks) {
224
            $callbacks->each->__invoke();
225
        };
226
    }
227
228
    public function reFillValue($model, ?string $attribute = null): static
229
    {
230
        $attribute   = $attribute ?? $this->attribute;
231
        $this->value = $this->resolver->set($model, $attribute, $this->groups);
232
233
        return $this;
234
    }
235
236
    /**
237
     * Process an incoming POST Request
238
     */
239
    protected function syncAndFillGroups(NovaRequest $request, string $requestAttribute, $model): array
240
    {
241
        if (!($raw = $this->extractValueFromRequest($request, $requestAttribute))) {
242
            $this->fireRemoveCallbacks(GroupsCollection::make(), $request, $model);
243
            $this->groups = GroupsCollection::make();
244
245
            return [];
246
        }
247
248
        $callbacks = [];
249
        $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
            $layout     = $item['layout'];
251
            $key        = $item['key'];
252
            $attributes = $item['attributes'];
253
254
            $group = $this->findGroup($key) ?? $this->newGroup($layout, $key);
255
256
            $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
            $scope     = ScopedRequest::scopeFrom($request, $attributes, $key);
258
            $callbacks = array_merge($callbacks, $group->fillFromRequest($scope));
259
260
            return $group;
261
        })->filter();
262
263
        $this->fireRemoveCallbacks($newGroups, $request, $model);
264
265
        $this->groups = $newGroups;
266
267
        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
    protected function fireRemoveCallbacks(GroupsCollection $newGroups, NovaRequest $request, $model): static
277
    {
278
        $newGroupKeys = $newGroups->map(function ($item) {
279
            return $item->inUseKey();
280
        });
281
282
        $this->groups()->filter(function ($item) use ($newGroupKeys) {
283
            // Return only removed groups.
284
            return !$newGroupKeys->contains($item->inUseKey());
285
        })->fireRemoveCallback($this, $request, $model);
286
287
        return $this;
288
    }
289
290
    /**
291
     * Find the flexible's value in given request.
292
     */
293
    protected function extractValueFromRequest(NovaRequest $request, string $attribute): ?array
294
    {
295
        $value = $request->input($attribute);
296
297
        if (!$value) {
298
            return null;
299
        }
300
301
        if (!is_array($value)) {
302
            throw new \Exception('Unable to parse incoming Flexible content, data should be an array.');
303
        }
304
305
        return $value;
306
    }
307
308
    /**
309
     * Resolve all contained groups and their fields.
310
     */
311 1
    protected function resolveGroups(GroupsCollection $groups): GroupsCollection
312
    {
313 1
        return $groups->map(function (Layout $group) {
314 1
            return $group->getResolved();
315 1
        });
316
    }
317
318
    /**
319
     * Resolve all contained groups and their fields for display on index and
320
     * detail views.
321
     */
322 3
    protected function resolveGroupsForDisplay(GroupsCollection $groups): GroupsCollection
323
    {
324 3
        return $groups->map(function ($group) {
325 3
            return $group->getResolvedForDisplay();
326 3
        });
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 4
    protected function buildGroups($resource, string $attribute): GroupsCollection
338
    {
339 4
        return $this->groups = $this->resolver->get($resource, $attribute, $this->layouts);
340
    }
341
342
    public function findGroup(string $groupKey): ?Layout
343
    {
344
        return $this->groups->first(fn (Layout $group) => $group->isUseKey($groupKey));
345
    }
346
347 1
    public function findGroupRecursive(string $groupKey): ?Layout
348
    {
349
        /** @var Layout $group */
350 1
        foreach ($this->groups as $group) {
351 1
            if ($group->isUseKey($groupKey)) {
352 1
                return $group;
353
            }
354 1
            if ($foundSubsequenceGroup = $group->findFlexibleGroupRecursive($groupKey)) {
355
                return $foundSubsequenceGroup;
356
            }
357
        }
358
359 1
        return null;
360
    }
361
362
    /**
363
     * @return bool - true if group found and false if not found.
364
     */
365
    public function setAttributeRecursive(string $groupKey, string $fieldKey, mixed $newValue = null): bool
366
    {
367
        $isUpdated = false;
368
369
        $this->groups
370
            ->each(function (Layout $group) use ($groupKey, $fieldKey, $newValue, &$isUpdated) {
371
                if ($group->isUseKey($groupKey)) {
372
                    $group->setAttribute($fieldKey, $newValue);
373
                    $isUpdated = true;
374
375
                    // Break loop
376
                    return false;
377
                }
378
379
                if ($group->findGroupRecursiveAndSetAttribute($groupKey, $fieldKey, $newValue)) {
380
                    $isUpdated = true;
381
382
                    // Break loop
383
                    return false;
384
                }
385
            });
386
387
        return $isUpdated;
388
    }
389
390
    /**
391
     * Create a new group based on its key and layout.
392
     */
393
    protected function newGroup(string $layout, string $key): ?Layout
394
    {
395
        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
    public function getUpdateRules(NovaRequest $request): array
420
    {
421
        return array_merge_recursive(
422
            parent::getUpdateRules($request),
423
            $this->getFlexibleRules($request, 'update')
424
        );
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
    protected function getFlexibleRules(NovaRequest $request, ?string $type = null): array
436
    {
437
438
        if (!($value = $this->extractValueFromRequest($request, $this->attribute))) {
439
            return [];
440
        }
441
442
        $rules = $this->generateRules($request, $value, $type);
443
444
        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
            static::registerValidationKeys($rules);
451
452
            // Then, transform the rules into an array that's actually
453
            // usable by Laravel's Validator.
454
            $rules = array_map(fn ($field) => $field['rules'], $rules);
455
        }
456
457
        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
    protected function generateRules(NovaRequest $request, array $value = [], ?string $type = null): array
469
    {
470
        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
            $group = $this->newGroup($item['layout'], $item['key']);
472
473
            if (!$group) {
474
                return [];
475
            }
476
477
            $scope = ScopedRequest::scopeFrom($request, $item['attributes'], $item['key']);
478
479
            return $group->generateRules($scope, "{$this->attribute}.{$key}", $type);
480
        })
481
            ->collapse()
482
            ->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
    protected static function registerValidationKeys(array $rules): void
493
    {
494
        static::$validatedKeys = array_merge(
495
            static::$validatedKeys,
496
            array_filter(array_map(fn ($field) => $field['attribute']??null, $rules))
497
        );
498
    }
499
500
    /**
501
     * Return a previously registered validation key
502
     *
503
     * @param string $key
504
     * @return null|\NovaFlexibleContent\Http\FlexibleAttribute
505
     */
506
    public static function getValidationKey(string $key): ?Http\FlexibleAttribute
507
    {
508
        return static::$validatedKeys[$key] ?? null;
509
    }
510
511
}
512