Flexible::setAttributeRecursive()   A
last analyzed

Complexity

Conditions 3
Paths 1

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3.1105

Importance

Changes 0
Metric Value
cc 3
eloc 11
nc 1
nop 3
dl 0
loc 23
ccs 10
cts 13
cp 0.7692
crap 3.1105
rs 9.9
c 0
b 0
f 0
1
<?php
2
3
namespace NovaFlexibleContent;
4
5
use Illuminate\Support\Str;
6
use Laravel\Nova\Contracts\Downloadable;
7
use Laravel\Nova\Fields\Field;
8
use Laravel\Nova\Fields\SupportsDependentFields;
9
use Laravel\Nova\Http\Requests\NovaRequest;
10
use NovaFlexibleContent\Http\ScopedRequest;
11
use NovaFlexibleContent\Layouts\Collections\GroupsCollection;
12
use NovaFlexibleContent\Layouts\Collections\LayoutsCollection as LayoutsCollection;
13
use NovaFlexibleContent\Layouts\Layout;
14
use NovaFlexibleContent\Nova\Fields\TraitsForFlexible\HasGroupRemovingConfirmation;
15
use NovaFlexibleContent\Nova\Fields\TraitsForFlexible\HasGroupsLimits;
16
use NovaFlexibleContent\Nova\Fields\TraitsForFlexible\HasLayoutsMenu;
17
use NovaFlexibleContent\Nova\Fields\TraitsForFlexible\HasOriginalModel;
18
use NovaFlexibleContent\Nova\Fields\TraitsForFlexible\HasPreset;
19
use NovaFlexibleContent\Nova\Fields\TraitsForFlexible\HasResolver;
20
21
class Flexible extends Field implements Downloadable
22
{
23
    use SupportsDependentFields;
24
    use HasResolver;
25
    use HasLayoutsMenu;
26
    use HasGroupsLimits;
27
    use HasGroupRemovingConfirmation;
28
    use HasPreset;
29
    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

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

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

156
        /** @scrutinizer ignore-deprecated */ $this->registerOriginModel($resource);
Loading history...
157
158 10
        $this->buildGroups($resource, $attribute);
159
160 10
        $this->value = $this->resolveGroups($this->groups);
161
    }
162
163
    /**
164
     * Resolve the field's value for display on index and detail views.
165
     *
166
     * @param mixed $resource
167
     * @param string|null $attribute
168
     * @return void
169
     */
170 4
    public function resolveForDisplay($resource, $attribute = null)
171
    {
172 4
        $attribute = $attribute ?? $this->attribute;
173
174 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

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

214
        /** @scrutinizer ignore-deprecated */ $this->registerOriginModel($model);
Loading history...
215
216 3
        $this->buildGroups($model, $attribute);
217
218 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

218
        $callbacks = GroupsCollection::make(/** @scrutinizer ignore-type */ $this->syncAndFillGroups($request, $requestAttribute, $model));
Loading history...
219
220 3
        $this->reFillValue($model, $attribute);
221
222 3
        if ($callbacks->isEmpty()) {
223 3
            return;
224
        }
225
226
        return function () use ($callbacks) {
227
            $callbacks->each->__invoke();
228
        };
229
    }
230
231 4
    public function reFillValue($model, ?string $attribute = null): static
232
    {
233 4
        $attribute   = $attribute ?? $this->attribute;
234 4
        $this->value = $this->resolver->set($model, $attribute, $this->groups);
235
236 4
        return $this;
237
    }
238
239
    /**
240
     * Process an incoming POST Request
241
     */
242 3
    protected function syncAndFillGroups(NovaRequest $request, string $requestAttribute, $model): array
243
    {
244 3
        if (!($raw = $this->extractValueFromRequest($request, $requestAttribute))) {
245
            $this->fireRemoveCallbacks(GroupsCollection::make(), $request, $model);
246
            $this->groups = GroupsCollection::make();
247
248
            return [];
249
        }
250
251 3
        $callbacks = [];
252 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

252
        $newGroups = GroupsCollection::make(/** @scrutinizer ignore-type */ $raw)->map(function ($item, $key) use ($request, &$callbacks) {
Loading history...
253 3
            $layout     = $item['layout'];
254 3
            $key        = $item['key'];
255 3
            $attributes = $item['attributes'];
256
257 3
            $group = $this->findGroup($key) ?? $this->newGroup($layout, $key);
258
259 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

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

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