Passed
Pull Request — 2.x (#1421)
by Quentin
09:12
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;
0 ignored issues
show
Bug introduced by
The type A17\Twill\Helpers\TwillBlock was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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
            $blockCmsData = app(BlockRepository::class)->buildFromCmsArray($block);
108 21
109
            /** @var \A17\Twill\Services\Blocks\Block $blockInstance */
110 21
            $blockInstance = $blockCmsData['instance'];
111 21
112
            // Figure out if the class has translations.
113 2
            $handleTranslations = property_exists($object, 'translatedAttributes');
114 2
115 2
            try {
116 2
                $this->validate(
117
                    $block['content'],
118 2
                    $block['id'],
119
                    $blockInstance->getRules(),
120
                    $handleTranslations ? $blockInstance->getRulesForTranslatedFields() : []
121 21
                );
122
            } catch (ValidationException $e) {
123
                $validator->errors()->merge($e->errors());
124
            }
125
        }
126
127
        if ($validator->errors()->isNotEmpty()) {
128
            throw new ValidationException($validator);
129
        }
130
131
        $blockRepository->bulkDelete($object->blocks()->pluck('id')->toArray());
132 2
        $this->getBlocks($object, $fields)->each(function ($block) use ($blockRepository) {
133
            $this->createBlock($blockRepository, $block);
134 2
        });
135
    }
136 2
137
    private function validate(array $formData, int $id, array $basicRules, array $translatedFieldRules): void
138
    {
139
        $finalValidator = Validator::make([], []);
140
        foreach ($translatedFieldRules as $field => $rules) {
141
            foreach (config('translatable.locales') as $locale) {
142
                $data = $formData[$field][$locale] ?? null;
143
                $validator = Validator::make([$field => $data], [$field => $rules]);
144
                foreach ($validator->messages()->getMessages() as $key => $errors) {
145
                    foreach ($errors as $error) {
146
                        $finalValidator->getMessageBag()->add("blocks.$id" . "[$key][$locale]", $error);
147 2
                        $finalValidator->getMessageBag()->add("blocks.$locale", 'Failed');
148
                    }
149
                }
150
            }
151
        }
152
        foreach ($basicRules as $field => $rules) {
153
            $validator = Validator::make([$field => $formData[$field] ?? null], [$field => $rules]);
154
            foreach ($validator->messages()->getMessages() as $key => $errors) {
155
                foreach ($errors as $error) {
156 2
                    $finalValidator->getMessageBag()->add("blocks[$id][$key]", $error);
157
                }
158 2
            }
159 2
        }
160
161 2
        if ($finalValidator->errors()->isNotEmpty()) {
162
            throw new ValidationException($finalValidator);
163
        }
164
    }
165
166
    /**
167
     * Create a block from formFields, and recursively create it's child blocks
168
     *
169 5
     * @param \A17\Twill\Repositories\BlockRepository $blockRepository
170
     * @param array $blockFields
171 5
     * @return \A17\Twill\Models\Block $blockCreated
172
     */
173 5
    private function createBlock(BlockRepository $blockRepository, $blockFields)
174
    {
175 5
        $blockCreated = $blockRepository->create($blockFields);
176
177 5
        // Handle child blocks
178
        $blockFields['blocks']->each(function ($childBlock) use ($blockCreated, $blockRepository) {
179 1
            $childBlock['parent_id'] = $blockCreated->id;
180 1
            $this->createBlock($blockRepository, $childBlock);
181 1
        });
182
183 1
        return $blockCreated;
184
    }
185
186
    /**
187
     * @param \A17\Twill\Models\Model $object
188 1
     * @param array $fields
189 1
     * @return \Illuminate\Support\Collection
190 1
     */
191 1
    private function getBlocks($object, $fields)
192
    {
193
        $blocks = Collection::make();
194 1
        if (isset($fields['blocks']) && is_array($fields['blocks'])) {
195
            foreach ($fields['blocks'] as $index => $block) {
196
                $block = $this->buildBlock($block, $object);
197
                $block['position'] = $index + 1;
198
                $block['blocks'] = $this->getChildBlocks($object, $block);
199
200
                $blocks->push($block);
201 1
            }
202 1
        }
203
        return $blocks;
204
    }
205
206 1
    /**
207 1
     * Recursively generate child blocks from the fields of a block
208 1
     *
209
     * @param \A17\Twill\Models\Model $object
210 1
     * @param array $parentBlockFields
211 1
     * @return \Illuminate\Support\Collection
212
     */
213 1
    private function getChildBlocks($object, $parentBlockFields)
214
    {
215 1
        $childBlocksList = Collection::make();
216
217 1
        foreach ($parentBlockFields['blocks'] as $childKey => $childBlocks) {
218
            foreach ($childBlocks as $index => $childBlock) {
219 1
                $childBlock = $this->buildBlock($childBlock, $object, true);
220
                $childBlock['child_key'] = $childKey;
221
                $childBlock['position'] = $index + 1;
222
                $childBlock['editor_name'] = $parentBlockFields['editor_name'] ?? 'default';
223
                $childBlock['blocks'] = $this->getChildBlocks($object, $childBlock);
224
225
                $childBlocksList->push($childBlock);
226
            }
227
        }
228
229
        return $childBlocksList;
230
    }
231
232
    /**
233
     * @param array $block
234
     * @param \A17\Twill\Models\Model $object
235
     * @param bool $repeater
236
     * @return array
237 1
     */
238
    private function buildBlock($block, $object, $repeater = false)
239 1
    {
240
        $block['blockable_id'] = $object->id;
241
        $block['blockable_type'] = $object->getMorphClass();
242
243
        return app(BlockRepository::class)->buildFromCmsArray($block, $repeater);
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
                $blockTypeConfig = $blocksList[$block->type] ?? null;
261
262 5
                if (is_null($blockTypeConfig)) {
263
                    continue;
264
                }
265
266 5
                $blockItem = [
267
                    'id' => $block->id,
268
                    'type' => $blockTypeConfig['component'],
269
                    'title' => $blockTypeConfig['title'],
270
                    'name' => $block->editor_name ?? 'default',
271 5
                    'titleField' => $blockTypeConfig['titleField'],
272
                    'hideTitlePrefix' => $blockTypeConfig['hideTitlePrefix'],
273
                    'attributes' => $blockTypeConfig['attributes'] ?? [],
274
                ];
275
276
                if ($isInRepeater) {
277
                    $fields['blocksRepeaters']["blocks-{$block->parent_id}_{$block->child_key}"][] = $blockItem + [
278
                        'trigger' => $blockTypeConfig['trigger'],
279
                    ] + (isset($blockTypeConfig['max']) ? [
280
                        'max' => $blockTypeConfig['max'],
281
                    ] : []);
282
                } else {
283
                    $fields['blocks'][$blockItem['name']][] = $blockItem + [
284
                        'icon' => $blockTypeConfig['icon'],
285
                    ];
286
                }
287
288
                $fields['blocksFields'][] = Collection::make($block['content'])->filter(function ($value, $key) {
289
                    return $key !== "browsers";
290
                })->map(function ($value, $key) use ($block) {
291
                    return [
292
                        'name' => "blocks[$block->id][$key]",
293
                        'value' => $value,
294
                    ];
295
                })->filter()->values()->toArray();
296
297
                $blockFormFields = app(BlockRepository::class)->getFormFields($block);
298
299
                $medias = $blockFormFields['medias'];
300
301
                if ($medias) {
302
                    if (config('twill.media_library.translated_form_fields', false)) {
303
                        $fields['blocksMedias'][] = Collection::make($medias)->mapWithKeys(function ($mediasByLocale, $locale) use ($block) {
304
                            return Collection::make($mediasByLocale)->mapWithKeys(function ($value, $key) use ($block, $locale) {
305
                                return [
306
                                    "blocks[$block->id][$key][$locale]" => $value,
307
                                ];
308
                            });
309
                        })->filter()->toArray();
310
                    } else {
311
                        $fields['blocksMedias'][] = Collection::make($medias)->mapWithKeys(function ($value, $key) use ($block) {
312
                            return [
313
                                "blocks[$block->id][$key]" => $value,
314
                            ];
315
                        })->filter()->toArray();
316
                    }
317
                }
318
319
                $files = $blockFormFields['files'];
320
321
                if ($files) {
322
                    Collection::make($files)->each(function ($rolesWithFiles, $locale) use (&$fields, $block) {
323
                        $fields['blocksFiles'][] = Collection::make($rolesWithFiles)->mapWithKeys(function ($files, $role) use ($locale, $block) {
324
                            return [
325
                                "blocks[$block->id][$role][$locale]" => $files,
326
                            ];
327
                        })->toArray();
328
                    });
329
                }
330
331
                if (isset($block['content']['browsers'])) {
332
                    $fields['blocksBrowsers'][] = $this->getBlockBrowsers($block);
333
                }
334
            }
335
336
            if ($fields['blocksFields'] ?? false) {
337
                $fields['blocksFields'] = call_user_func_array('array_merge', $fields['blocksFields'] ?? []);
338
            }
339
340
            if ($fields['blocksMedias'] ?? false) {
341
                $fields['blocksMedias'] = call_user_func_array('array_merge', $fields['blocksMedias'] ?? []);
342
            }
343
344
            if ($fields['blocksFiles'] ?? false) {
345
                $fields['blocksFiles'] = call_user_func_array('array_merge', $fields['blocksFiles'] ?? []);
346
            }
347
348
            if ($fields['blocksBrowsers'] ?? false) {
349
                $fields['blocksBrowsers'] = call_user_func_array('array_merge', $fields['blocksBrowsers'] ?? []);
350
            }
351
        }
352
353
        return $fields;
354
    }
355
356
    /**
357
     * @param \A17\Twill\Models\Block $block
358
     * @return array
359
     */
360
    protected function getBlockBrowsers($block)
361
    {
362
        return Collection::make($block['content']['browsers'])->mapWithKeys(function ($ids, $relation) use ($block) {
363
            if (Schema::hasTable(config('twill.related_table', 'twill_related')) && $block->getRelated($relation)->isNotEmpty()) {
364
                $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

364
                /** @scrutinizer ignore-call */ 
365
                $items = $this->getFormFieldsForRelatedBrowser($block, $relation);
Loading history...
365
                foreach ($items as &$item) {
366
                    if (!isset($item['edit'])) {
367
                        try {
368
                            $item['edit'] = moduleRoute(
369
                                $relation,
370
                                config('twill.block_editor.browser_route_prefixes.' . $relation),
371
                                'edit',
372
                                $item['id']
373
                            );
374
                        } catch (RouteNotFoundException $e) {
375
                            report($e);
376
                            Log::notice(
377
                                "Twill warning: The url for the \"{$relation}\" browser items can't " .
378
                                "be resolved. You might be missing a {$relation} key in your " .
379
                                "twill.block_editor.browser_route_prefixes configuration."
380
                            );
381
                        }
382
                    }
383
                }
384
            } else {
385
                $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

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