Passed
Push — develop ( 945018...5e0e8c )
by Richard
15:44
created

Block   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 449
Duplicated Lines 0 %

Importance

Changes 19
Bugs 0 Features 2
Metric Value
wmc 46
eloc 106
c 19
b 0
f 2
dl 0
loc 449
rs 8.72

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 5
A component() 0 3 1
A getFieldType() 0 3 1
A views() 0 14 1
A isChildOf() 0 3 1
A jsonSerialize() 0 3 1
A hasChildBlock() 0 4 1
A isAncestorOf() 0 3 1
A getIterator() 0 2 1
A page() 0 7 2
A editorLink() 0 7 3
A render() 0 3 1
A __get() 0 16 6
A getRelation() 0 23 3
A parent() 0 3 1
A getCasts() 0 3 1
A preprocess() 0 15 3
A ancestorComponentName() 0 3 1
A content() 0 11 3
A has() 0 3 1
A processFields() 0 3 1
A __toString() 0 3 1
A renderUsing() 0 6 2
A inverseRelation() 0 33 4

How to fix   Complexity   

Complex Class

Complex classes like Block often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Block, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
4
namespace Riclep\Storyblok;
5
6
use Illuminate\Support\Arr;
7
use Illuminate\Support\Collection;
8
use Illuminate\Support\Str;
9
use Illuminate\View\View;
10
use Riclep\Storyblok\Exceptions\UnableToRenderException;
11
use Riclep\Storyblok\Fields\Asset;
12
use Riclep\Storyblok\Fields\Image;
13
use Riclep\Storyblok\Fields\MultiAsset;
14
use Riclep\Storyblok\Fields\RichText;
15
use Riclep\Storyblok\Fields\Table;
16
use Riclep\Storyblok\Traits\HasChildClasses;
17
use Riclep\Storyblok\Traits\HasMeta;
18
use Riclep\Storyblok\Traits\HasSettings;
19
use Storyblok\ApiException;
20
21
22
class Block implements \IteratorAggregate, \JsonSerializable
23
{
24
	use HasChildClasses;
25
	use HasMeta;
26
	use HasSettings;
27
28
	/**
29
	 * @var bool resolve UUID relations automatically
30
	 */
31
	public bool $_autoResolveRelations = false;
32
33
	/**
34
	 * @var array list of field names containing relations to resolve
35
	 */
36
	public array $_resolveRelations = [];
37
38
	/**
39
	 * @var bool Remove unresolved relations such as those that 404
40
	 */
41
	public bool $_filterRelations = true;
42
43
	/**
44
	 * @var array the path of nested components
45
	 */
46
	public array $_componentPath = [];
47
48
	/**
49
	 * @var array the path of nested components
50
	 */
51
	protected array $_casts = [];
52
53
	/**
54
	 * @var Collection all the fields for the Block
55
	 */
56
	protected Collection $_fields;
57
58
	/**
59
	 * @var Page|Block reference to the parent Block or Page
60
	 */
61
	protected mixed $_parent;
62
63
	/**
64
	 * @var array default values for fields
65
	 */
66
	protected array $_defaults = [];
67
68
	/**
69
	 * Takes the Block’s content and a reference to the parent
70
	 * @param $content
71
	 * @param $parent
72
	 */
73
	public function __construct($content, $parent = null)
74
	{
75
		$this->_parent = $parent;
76
		$this->preprocess($content);
77
78
		if ($parent) {
79
			$this->_componentPath = array_merge($parent->_componentPath, [Str::lower($this->meta()['component'])]);
80
		}
81
82
		$this->processFields();
83
84
		if (method_exists($this, 'fieldsReady')) {
85
			$this->fieldsReady();
86
		}
87
88
		// run automatic traits - methods matching initTraitClassName()
89
		foreach (class_uses_recursive($this) as $trait) {
90
			if (method_exists($this, $method = 'init' . class_basename($trait))) {
91
				$this->{$method}();
92
			}
93
		}
94
	}
95
96
	/**
97
	 * Returns the every field of content
98
	 *
99
	 * @return Collection
100
	 */
101
	public function content(): Collection
102
	{
103
		$fields = $this->_fields;
104
105
		foreach ($fields as $key => $field) {
106
			if ($field === null) {
107
				$fields[$key] = $this->_defaults[$key] ?? null;
108
			}
109
		}
110
111
		return $fields;
112
	}
113
114
	/**
115
	 * Checks if this Block’s fields contain the specified key
116
	 *
117
	 * @param $key
118
	 * @return bool
119
	 */
120
	public function has($key): bool
121
	{
122
		return $this->_fields->has($key);
123
	}
124
125
	/**
126
	 * Checks if a ‘Blocks’ fieldtype contains a specific block component
127
	 * Pass the $field that contains the blocks and the component type to search for
128
	 *
129
	 * @param $field
130
	 * @param $component
131
	 * @return boolean
132
	 */
133
	public function hasChildBlock($field, $component): bool
134
	{
135
		return $this->content()[$field]->contains(function($item) use ($component) {
136
			return $item->meta('component') === $component;
137
		});
138
	}
139
140
	/**
141
	 * Returns the parent Block
142
	 *
143
	 * @return Block
144
	 */
145
	public function parent(): Block|Page|null
146
	{
147
		return $this->_parent;
148
	}
149
150
	/**
151
	 * Returns the page this Block belongs to
152
	 *
153
	 * @return Block
154
	 */
155
	public function page(): Block|Page|null
156
	{
157
		if ($this->parent() instanceof Page) {
158
			return $this->parent();
159
		}
160
161
		return $this->parent()->page();
162
	}
163
164
	/**
165
	 * Returns the first matching view, passing it the Block and optional data
166
	 *
167
	 * @param array $with
168
	 * @return View
169
	 * @throws UnableToRenderException
170
	 */
171
	public function render(array $with = []): View
172
	{
173
		return $this->renderUsing($this->views(), $with);
174
	}
175
176
	/**
177
	 * Pass an array of views rendering the first match, passing it the Block and optional data
178
	 *
179
	 * @param array|string $views
180
	 * @param array $with
181
	 * @return View
182
	 * @throws UnableToRenderException
183
	 */
184
	public function renderUsing(array|string $views, array $with = []): View
185
	{
186
		try {
187
			return view()->first(Arr::wrap($views), array_merge(['block' => $this], $with));
188
		} catch (\Exception $exception) {
189
			throw new UnableToRenderException('None of the views in the given array exist.', $this);
190
		}
191
	}
192
193
	/**
194
	 * Returns an array of possible views for the current Block based on
195
	 * it’s $componentPath match the component prefixed by each of it’s
196
	 * ancestors in turn, starting with the closest, for example:
197
	 *
198
	 * $componentPath = ['page', 'parent', 'child', 'this_block'];
199
	 *
200
	 * Becomes a list of possible views like so:
201
	 * ['child.this_block', 'parent.this_block', 'page.this_block'];
202
	 *
203
	 * Override this method with your custom implementation for
204
	 * ultimate control
205
	 *
206
	 * @return array
207
	 */
208
	public function views(): array
209
	{
210
		$componentPath = $this->_componentPath;
211
		array_pop($componentPath);
212
213
		$views = array_map(function($path) {
214
			return config('storyblok.view_path') . 'blocks.' . $path . '.' . $this->component();
215
		}, $componentPath);
216
217
		$views = array_reverse($views);
218
219
		$views[] = config('storyblok.view_path') . 'blocks.' . $this->component();
220
221
		return $views;
222
	}
223
224
	/**
225
	 * Returns a component X generations previous
226
	 *
227
	 * @param $generation int
228
	 * @return mixed
229
	 */
230
	public function ancestorComponentName(int $generation): mixed
231
	{
232
		return $this->_componentPath[count($this->_componentPath) - ($generation + 1)];
233
	}
234
235
	/**
236
	 * Checks if the current component is a child of another
237
	 *
238
	 * @param $parent string
239
	 * @return bool
240
	 */
241
	public function isChildOf(string $parent): bool
242
	{
243
		return $this->_componentPath[count($this->_componentPath) - 2] === $parent;
244
	}
245
246
	/**
247
	 * Checks if the component is an ancestor of another
248
	 *
249
	 * @param $parent string
250
	 * @return bool
251
	 */
252
	public function isAncestorOf(string $parent): bool
253
	{
254
		return in_array($parent, $this->parent()->_componentPath, true);
255
	}
256
257
	/**
258
	 * Returns the current Block’s component name from Storyblok
259
	 *
260
	 * @return string
261
	 */
262
	public function component(): string
263
	{
264
		return $this->_meta['component'];
265
	}
266
267
268
	/**
269
	 * Returns the HTML comment required for making this Block clickable in
270
	 * Storyblok’s visual editor. Don’t forget to set comments to true in
271
	 * your Vue.js app configuration.
272
	 *
273
	 * @return string
274
	 */
275
	public function editorLink(): string
276
	{
277
		if (array_key_exists('_editable', $this->_meta) && config('storyblok.edit_mode')) {
278
			return $this->_meta['_editable'];
279
		}
280
281
		return '';
282
	}
283
284
285
	/**
286
	 * Magic accessor to pull content from the _fields collection. Works just like
287
	 * Laravel’s model accessors. Matches public methods with the follow naming
288
	 * convention getSomeFieldAttribute() - called via $block->some_field
289
	 *
290
	 * @param $key
291
	 * @return null|string
292
	 */
293
	public function __get($key) {
294
		$accessor = 'get' . Str::studly($key) . 'Attribute';
295
296
		if (method_exists($this, $accessor)) {
297
			return $this->$accessor();
298
		}
299
300
		if (array_key_exists($key, $this->_defaults) && $this->has($key) && !$this->_fields[$key]) {
301
			return $this->_defaults[$key];
302
		}
303
304
		if ($this->has($key)) {
305
			return $this->_fields[$key];
306
		}
307
308
		return null;
309
	}
310
311
	/**
312
	 * Casts the Block as a string - json serializes the $_fields Collection
313
	 *
314
	 * @return string
315
	 */
316
	public function __toString(): string
317
	{
318
		return (string) $this->jsonSerialize();
319
	}
320
321
	/**
322
	 * Loops over every field to get the ball rolling
323
	 */
324
	private function processFields(): void
325
	{
326
		$this->_fields->transform(fn($field, $key) => $this->getFieldType($field, $key));
327
	}
328
329
	/**
330
	 * Converts fields into Field Classes based on various properties of their content
331
	 *
332
	 * @param $field
333
	 * @param $key
334
	 * @return array|Collection|mixed|Asset|Image|MultiAsset|RichText|Table
335
	 * @throws \Storyblok\ApiException
336
	 */
337
	private function getFieldType($field, $key): mixed
338
	{
339
		return (new FieldFactory())->build($this, $field, $key);
340
	}
341
342
	/**
343
	 * Storyblok returns fields and other meta content at the same level so
344
	 * let’s do a little tidying up first
345
	 *
346
	 * @param $content
347
	 */
348
	private function preprocess($content): void
349
	{
350
		// run pre-process traits - methods matching preprocessTraitClassName()
351
		foreach (class_uses_recursive($this) as $trait) {
352
			if (method_exists($this, $method = 'preprocess' . class_basename($trait))) {
353
				$content = $this->{$method}($content);
354
			}
355
		}
356
357
		$fields = ['_editable', '_uid', 'component'];
358
359
		$this->_fields = collect(array_diff_key($content, array_flip($fields)));
0 ignored issues
show
Bug introduced by
array_diff_key($content, array_flip($fields)) of type array is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $value of collect(). ( Ignorable by Annotation )

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

359
		$this->_fields = collect(/** @scrutinizer ignore-type */ array_diff_key($content, array_flip($fields)));
Loading history...
360
361
		// remove non-content keys
362
		$this->_meta = array_intersect_key($content, array_flip($fields));
363
	}
364
365
	/**
366
	 * Casting Block to JSON
367
	 *
368
	 * @return Collection|mixed
369
	 */
370
	public function jsonSerialize(): mixed
371
	{
372
		return $this->content();
373
	}
374
375
	/**
376
	 * Let’s up loop over the fields in Blade without needing to
377
	 * delve deep into the content collection
378
	 *
379
	 * @return \Traversable
380
	 */
381
	public function getIterator(): \Traversable {
382
		return $this->_fields;
383
	}
384
385
	/**
386
	 * @param RequestStory $requestStory
387
	 * @param $relation
388
	 * @param $className
389
	 * @return mixed|null
390
	 */
391
	public function getRelation(RequestStory $requestStory, $relation, $className = null): mixed
392
	{
393
		try {
394
			$response = $requestStory->get($relation);
395
396
			if (!$className) {
397
				$class = $this->getChildClassName('Block', $response['content']['component']);
398
			} else {
399
				$class = $className;
400
			}
401
402
			$relationClass = new $class($response['content'], $this);
403
404
			$relationClass->addMeta([
405
				'name' => $response['name'],
406
				'published_at' => $response['published_at'],
407
				'full_slug' => $response['full_slug'],
408
				'page_uuid' => $relation,
409
			]);
410
411
			return $relationClass;
412
		} catch (ApiException $e) {
413
			return null;
414
		}
415
	}
416
417
	/**
418
	 * Returns an inverse relationship to the current Block. For example if Service has a Multi-Option field
419
	 * relationship to People, on People you can request all the Services it has been related to
420
	 *
421
	 * @param string $foreignRelationshipField
422
	 * @param string $foreignRelationshipType
423
	 * @param array|null $components
424
	 * @param array|null $options
425
	 * @return array
426
	 */
427
	public function inverseRelation(string $foreignRelationshipField, string $foreignRelationshipType = 'multi', array $components = null, array $options = null): array
428
	{
429
		$storyblokClient = resolve('Storyblok\Client');
430
431
		$type = 'any_in_array';
432
433
		if ($foreignRelationshipType === 'single') {
434
			$type = 'in';
435
		}
436
437
		$query = [
438
			'filter_query' => [
439
				$foreignRelationshipField => [$type => $this->meta('page_uuid') ?? $this->page()->uuid()]
440
			],
441
		];
442
443
		if ($components) {
444
			$query = array_merge_recursive($query, [
445
				'filter_query' => [
446
					'component' => ['in' => $components],
447
				]
448
			]);
449
		}
450
451
		if ($options) {
452
			$query = array_merge_recursive($query, $options);
453
		}
454
455
		$storyblokClient->getStories($query);
456
457
		return [
458
			'headers' => $storyblokClient->getHeaders(),
459
			'stories' => $storyblokClient->getBody()['stories'],
460
		];
461
	}
462
463
	/**
464
	 * Returns the casts on this Block
465
	 *
466
	 * @return array
467
	 */
468
	public function getCasts(): array
469
	{
470
		return $this->_casts;
471
	}
472
}