Passed
Pull Request — 2.x (#1308)
by Harings
07:04
created

HandleBlocks::validate()   B

Complexity

Conditions 9
Paths 40

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 22.1482

Importance

Changes 0
Metric Value
cc 9
eloc 16
nc 40
nop 4
dl 0
loc 26
ccs 5
cts 11
cp 0.4545
crap 22.1482
rs 8.0555
c 0
b 0
f 0
1
<?php
2
3
namespace A17\Twill\Repositories\Behaviors;
4
5
use A17\Twill\Helpers\TwillBlock;
6
use A17\Twill\Models\Behaviors\HasMedias;
7
use A17\Twill\Models\Model;
8
use A17\Twill\Repositories\BlockRepository;
9
use A17\Twill\Services\Blocks\BlockCollection;
10
use Illuminate\Support\Collection;
11
use Illuminate\Support\Facades\Validator;
12
use Illuminate\Validation\ValidationException;
13
use Log;
14
use Schema;
15
use Symfony\Component\Routing\Exception\RouteNotFoundException;
16
17
trait HandleBlocks
18 2
{
19
    /**
20 2
     * @param \A17\Twill\Models\Model $object
21
     * @param array $fields
22
     * @param int $fakeBlockId
23
     * @param int|null $parentId
24 2
     * @param \Illuminate\Support\Collection|null $blocksFromFields
25 2
     * @param \Illuminate\Support\Collection|null $mainCollection
26 2
     * @return \A17\Twill\Models\Model|null
27 2
     */
28 2
    public function hydrateHandleBlocks(
29
        $object,
30 2
        $fields,
31 2
        &$fakeBlockId = 0,
32
        $parentId = null,
33
        $blocksFromFields = null,
34 2
        $mainCollection = null
35 2
    ) {
36 2
        if ($this->shouldIgnoreFieldBeforeSave('blocks')) {
0 ignored issues
show
Bug introduced by
It seems like shouldIgnoreFieldBeforeSave() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

36
        if ($this->/** @scrutinizer ignore-call */ shouldIgnoreFieldBeforeSave('blocks')) {
Loading history...
37 2
            return null;
38
        }
39
40 2
        $firstItem = false;
41
        if ($mainCollection === null) {
42 2
            $firstItem = true;
43
            $mainCollection = Collection::make();
44 2
        }
45 1
        if ($blocksFromFields === null) {
46
            $blocksFromFields = $this->getBlocks($object, $fields);
47
        }
48 1
49
        $blockRepository = app(BlockRepository::class);
50 1
        $blocksCollection = $this->getChildrenBlocks(
51 1
            $blocksFromFields,
52 1
            $blockRepository,
53 1
            $parentId,
54 1
            $fakeBlockId,
55
            $mainCollection
56
        );
57 1
        $object->setRelation('blocks', $firstItem ? $mainCollection : $blocksCollection);
58 1
        return $object;
59
    }
60 2
61
    protected function getChildrenBlocks($blocks, $blockRepository, $parentId, &$fakeBlockId, $mainCollection)
62
    {
63
        $childBlocksCollection = Collection::make();
64
65
        foreach ($blocks as $childBlock) {
66
            if ($parentId) {
67
                $childBlock['parent_id'] = $parentId;
68 21
            }
69
            $newChildBlock = $blockRepository->createForPreview($childBlock);
70 21
71
            $fakeBlockId++;
72
            $newChildBlock->id = $fakeBlockId;
73
            if (!empty($childBlock['blocks'])) {
74 21
                $childBlockHydrated = $this->hydrateHandleBlocks(
75
                    $newChildBlock,
76 21
                    $childBlock,
77 21
                    $fakeBlockId,
78 2
                    $newChildBlock->id,
79 21
                    $childBlock['blocks'],
80 21
                    $mainCollection
81
                );
82
                $newChildBlock->setRelation('children', $childBlockHydrated->blocks);
0 ignored issues
show
Bug introduced by
The property blocks does not seem to exist on A17\Twill\Models\Model. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
83
            }
84
85
            $mainCollection->push($newChildBlock);
86
            $childBlocksCollection->push($newChildBlock);
87
        }
88
        return $childBlocksCollection;
89
    }
90 2
91
    /**
92 2
     * @param \A17\Twill\Models\Model $object
93
     * @param array $fields
94
     * @return void
95 2
     */
96
    public function afterSaveHandleBlocks(Model $object, array $fields): void
97
    {
98 2
        if ($this->shouldIgnoreFieldBeforeSave('blocks')) {
99
            return;
100 2
        }
101
102
        $blockRepository = app(BlockRepository::class);
103
104
        $validator = Validator::make([], []);
105
106
        foreach ($fields['blocks'] ?? [] as $block) {
107
            $blockArray = app(BlockRepository::class)->buildFromCmsArray($block);
108 21
            $helper = TwillBlock::getBlockClassForName($blockArray['type']);
109
            if ($helper) {
110 21
                try {
111 21
                    $this->validate(
112
                        $block['content'],
113 2
                        $block['id'],
114 2
                        $helper->getRules(),
115 2
                        $helper->getRulesForTranslatedFields()
116 2
                    );
117
                } catch (ValidationException $e) {
118 2
                    $validator->errors()->merge($e->errors());
119
                }
120
            }
121 21
        }
122
123
        if ($validator->errors()->isNotEmpty()) {
124
            throw new ValidationException($validator);
125
        }
126
127
        $blockRepository->bulkDelete($object->blocks()->pluck('id')->toArray());
128
        $this->getBlocks($object, $fields)->each(function ($block) use ($blockRepository) {
129
            $this->createBlock($blockRepository, $block);
130
        });
131
    }
132 2
133
    private function validate(array $formData, int $id, array $basicRules, array $translatedFieldRules): void
134 2
    {
135
        $finalValidator = Validator::make([], []);
136 2
        foreach ($translatedFieldRules as $field => $rules) {
137
            foreach (config('translatable.locales') as $locale) {
138
                $data = $formData[$field][$locale] ?? null;
139
                $validator = Validator::make([$field => $data], [$field => $rules]);
140
                foreach ($validator->messages()->getMessages() as $key => $errors) {
141
                    foreach ($errors as $error) {
142
                        $finalValidator->getMessageBag()->add("blocks.$id" . "[$key][$locale]", $error);
143
                        $finalValidator->getMessageBag()->add("blocks.$locale", 'Failed');
144
                    }
145
                }
146
            }
147 2
        }
148
        foreach ($basicRules as $field => $rules) {
149
            $validator = Validator::make([$field => $formData[$field] ?? null], [$field => $rules]);
150
            foreach ($validator->messages()->getMessages() as $key => $errors) {
151
                foreach ($errors as $error) {
152
                    $finalValidator->getMessageBag()->add("blocks[$id][$key]", $error);
153
                }
154
            }
155
        }
156 2
157
        if ($finalValidator->errors()->isNotEmpty()) {
158 2
            throw new ValidationException($finalValidator);
159 2
        }
160
    }
161 2
162
    /**
163
     * Create a block from formFields, and recursively create it's child blocks
164
     *
165
     * @param \A17\Twill\Repositories\BlockRepository $blockRepository
166
     * @param array $blockFields
167
     * @return \A17\Twill\Models\Block $blockCreated
168
     */
169 5
    private function createBlock(BlockRepository $blockRepository, $blockFields)
170
    {
171 5
        $blockCreated = $blockRepository->create($blockFields);
172
173 5
        // Handle child blocks
174
        $blockFields['blocks']->each(function ($childBlock) use ($blockCreated, $blockRepository) {
175 5
            $childBlock['parent_id'] = $blockCreated->id;
176
            $this->createBlock($blockRepository, $childBlock);
177 5
        });
178
179 1
        return $blockCreated;
180 1
    }
181 1
182
    /**
183 1
     * @param \A17\Twill\Models\Model $object
184
     * @param array $fields
185
     * @return \Illuminate\Support\Collection
186
     */
187
    private function getBlocks($object, $fields)
188 1
    {
189 1
        $blocks = Collection::make();
190 1
        if (isset($fields['blocks']) && is_array($fields['blocks'])) {
191 1
            foreach ($fields['blocks'] as $index => $block) {
192
                $block = $this->buildBlock($block, $object);
193
                $block['position'] = $index + 1;
194 1
                $block['blocks'] = $this->getChildBlocks($object, $block);
195
196
                $blocks->push($block);
197
            }
198
        }
199
        return $blocks;
200
    }
201 1
202 1
    /**
203
     * Recursively generate child blocks from the fields of a block
204
     *
205
     * @param \A17\Twill\Models\Model $object
206 1
     * @param array $parentBlockFields
207 1
     * @return \Illuminate\Support\Collection
208 1
     */
209
    private function getChildBlocks($object, $parentBlockFields)
210 1
    {
211 1
        $childBlocksList = Collection::make();
212
213 1
        foreach ($parentBlockFields['blocks'] as $childKey => $childBlocks) {
214
            foreach ($childBlocks as $index => $childBlock) {
215 1
                $childBlock = $this->buildBlock($childBlock, $object, true);
216
                $childBlock['child_key'] = $childKey;
217 1
                $childBlock['position'] = $index + 1;
218
                $childBlock['editor_name'] = $parentBlockFields['editor_name'] ?? 'default';
219 1
                $childBlock['blocks'] = $this->getChildBlocks($object, $childBlock);
220
221
                $childBlocksList->push($childBlock);
222
            }
223
        }
224
225
        return $childBlocksList;
226
    }
227
228
    /**
229
     * @param array $block
230
     * @param \A17\Twill\Models\Model $object
231
     * @param bool $repeater
232
     * @return array
233
     */
234
    private function buildBlock($block, $object, $repeater = false)
235
    {
236
        $block['blockable_id'] = $object->id;
237 1
        $block['blockable_type'] = $object->getMorphClass();
238
239 1
        $block = app(BlockRepository::class)->buildFromCmsArray($block, $repeater);
240
241
        $block['helper'] = TwillBlock::getBlockClassForName($block['type']);
242
243
        return $block;
244
    }
245
246
    /**
247
     * @param \A17\Twill\Models\Model $object
248
     * @param array $fields
249 1
     * @return array
250
     */
251
    public function getFormFieldsHandleBlocks($object, $fields)
252
    {
253
        $fields['blocks'] = null;
254 5
255 1
        if ($object->has('blocks')) {
256
            $blocksList = app(BlockCollection::class)->list()->keyBy('name');
257
258 5
            foreach ($object->blocks as $block) {
0 ignored issues
show
Bug introduced by
The property blocks does not seem to exist on A17\Twill\Models\Model. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
259
                $isInRepeater = isset($block->parent_id);
260
                $configKey = $isInRepeater ? 'repeaters' : 'blocks';
0 ignored issues
show
Unused Code introduced by
The assignment to $configKey is dead and can be removed.
Loading history...
261
                $blockTypeConfig = $blocksList[$block->type] ?? null;
262 5
263
                if (is_null($blockTypeConfig)) {
264
                    continue;
265
                }
266 5
267
                $blockItem = [
268
                    'id' => $block->id,
269
                    'type' => $blockTypeConfig['component'],
270
                    'title' => $blockTypeConfig['title'],
271 5
                    'name' => $block->editor_name ?? 'default',
272
                    'titleField' => $blockTypeConfig['titleField'],
273
                    'hideTitlePrefix' => $blockTypeConfig['hideTitlePrefix'],
274
                    'attributes' => $blockTypeConfig['attributes'] ?? [],
275
                ];
276
277
                if ($isInRepeater) {
278
                    $fields['blocksRepeaters']["blocks-{$block->parent_id}_{$block->child_key}"][] = $blockItem + [
279
                        'trigger' => $blockTypeConfig['trigger'],
280
                    ] + (isset($blockTypeConfig['max']) ? [
281
                        'max' => $blockTypeConfig['max'],
282
                    ] : []);
283
                } else {
284
                    $fields['blocks'][$blockItem['name']][] = $blockItem + [
285
                        'icon' => $blockTypeConfig['icon'],
286
                    ];
287
                }
288
289
                $fields['blocksFields'][] = Collection::make($block['content'])->filter(function ($value, $key) {
290
                    return $key !== "browsers";
291
                })->map(function ($value, $key) use ($block) {
292
                    return [
293
                        'name' => "blocks[$block->id][$key]",
294
                        'value' => $value,
295
                    ];
296
                })->filter()->values()->toArray();
297
298
                $blockFormFields = app(BlockRepository::class)->getFormFields($block);
299
300
                $medias = $blockFormFields['medias'];
301
302
                if ($medias) {
303
                    if (config('twill.media_library.translated_form_fields', false)) {
304
                        $fields['blocksMedias'][] = Collection::make($medias)->mapWithKeys(function ($mediasByLocale, $locale) use ($block) {
305
                            return Collection::make($mediasByLocale)->mapWithKeys(function ($value, $key) use ($block, $locale) {
306
                                return [
307
                                    "blocks[$block->id][$key][$locale]" => $value,
308
                                ];
309
                            });
310
                        })->filter()->toArray();
311
                    } else {
312
                        $fields['blocksMedias'][] = Collection::make($medias)->mapWithKeys(function ($value, $key) use ($block) {
313
                            return [
314
                                "blocks[$block->id][$key]" => $value,
315
                            ];
316
                        })->filter()->toArray();
317
                    }
318
                }
319
320
                $files = $blockFormFields['files'];
321
322
                if ($files) {
323
                    Collection::make($files)->each(function ($rolesWithFiles, $locale) use (&$fields, $block) {
324
                        $fields['blocksFiles'][] = Collection::make($rolesWithFiles)->mapWithKeys(function ($files, $role) use ($locale, $block) {
325
                            return [
326
                                "blocks[$block->id][$role][$locale]" => $files,
327
                            ];
328
                        })->toArray();
329
                    });
330
                }
331
332
                if (isset($block['content']['browsers'])) {
333
                    $fields['blocksBrowsers'][] = $this->getBlockBrowsers($block);
334
                }
335
            }
336
337
            if ($fields['blocksFields'] ?? false) {
338
                $fields['blocksFields'] = call_user_func_array('array_merge', $fields['blocksFields'] ?? []);
339
            }
340
341
            if ($fields['blocksMedias'] ?? false) {
342
                $fields['blocksMedias'] = call_user_func_array('array_merge', $fields['blocksMedias'] ?? []);
343
            }
344
345
            if ($fields['blocksFiles'] ?? false) {
346
                $fields['blocksFiles'] = call_user_func_array('array_merge', $fields['blocksFiles'] ?? []);
347
            }
348
349
            if ($fields['blocksBrowsers'] ?? false) {
350
                $fields['blocksBrowsers'] = call_user_func_array('array_merge', $fields['blocksBrowsers'] ?? []);
351
            }
352
        }
353
354
        return $fields;
355
    }
356
357
    /**
358
     * @param \A17\Twill\Models\Block $block
359
     * @return array
360
     */
361
    protected function getBlockBrowsers($block)
362
    {
363
        return Collection::make($block['content']['browsers'])->mapWithKeys(function ($ids, $relation) use ($block) {
364
            if (Schema::hasTable(config('twill.related_table', 'twill_related')) && $block->getRelated($relation)->isNotEmpty()) {
365
                $items = $this->getFormFieldsForRelatedBrowser($block, $relation);
0 ignored issues
show
Bug introduced by
It seems like getFormFieldsForRelatedBrowser() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

365
                /** @scrutinizer ignore-call */ 
366
                $items = $this->getFormFieldsForRelatedBrowser($block, $relation);
Loading history...
366
                foreach ($items as &$item) {
367
                    if (!isset($item['edit'])) {
368
                        try {
369
                            $item['edit'] = moduleRoute(
370
                                $relation,
371
                                config('twill.block_editor.browser_route_prefixes.' . $relation),
372
                                'edit',
373
                                $item['id']
374
                            );
375
                        } catch (RouteNotFoundException $e) {
376
                            report($e);
377
                            Log::notice(
378
                                "Twill warning: The url for the \"{$relation}\" browser items can't " .
379
                                "be resolved. You might be missing a {$relation} key in your " .
380
                                "twill.block_editor.browser_route_prefixes configuration."
381
                            );
382
                        }
383
                    }
384
                }
385
            } else {
386
                $relationRepository = $this->getModelRepository($relation);
0 ignored issues
show
Bug introduced by
It seems like getModelRepository() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

386
                /** @scrutinizer ignore-call */ 
387
                $relationRepository = $this->getModelRepository($relation);
Loading history...
387
                $relatedItems = $relationRepository->get([], ['id' => $ids], [], -1);
388
                $sortedRelatedItems = array_flip($ids);
389
390
                foreach ($relatedItems as $item) {
391
                    $sortedRelatedItems[$item->id] = $item;
392
                }
393
394
                $items = Collection::make(array_values($sortedRelatedItems))->filter(function ($value) {
395
                    return is_object($value);
396
                })->map(function ($relatedElement) use ($relation) {
397
                    return [
398
                        'id' => $relatedElement->id,
399
                        'name' => $relatedElement->titleInBrowser ?? $relatedElement->title,
400
                        'edit' => moduleRoute($relation, config('twill.block_editor.browser_route_prefixes.' . $relation), 'edit', $relatedElement->id),
401
                    ] + (classHasTrait($relatedElement, HasMedias::class) ? [
402
                        'thumbnail' => $relatedElement->defaultCmsImage(['w' => 100, 'h' => 100]),
403
                    ] : []);
404
                })->toArray();
405
            }
406
            return [
407
                "blocks[$block->id][$relation]" => $items,
408
            ];
409
        })->filter()->toArray();
410
    }
411
}
412