Passed
Push — master ( beb509...f9654e )
by Richard
03:40 queued 11s
created

Block::views()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 13
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
4
namespace Riclep\Storyblok;
5
6
use Exception;
7
use Illuminate\Support\Collection;
8
use Illuminate\Support\Str;
9
use Illuminate\View\View;
10
use Riclep\Storyblok\Fields\Asset;
11
use Riclep\Storyblok\Fields\Image;
12
use Riclep\Storyblok\Fields\MultiAsset;
13
use Riclep\Storyblok\Fields\RichText;
14
use Riclep\Storyblok\Fields\Table;
15
use Riclep\Storyblok\Traits\CssClasses;
16
use Riclep\Storyblok\Traits\HasChildClasses;
17
use Riclep\Storyblok\Traits\HasMeta;
18
19
class Block implements \IteratorAggregate
20
{
21
	use CssClasses;
22
	use HasChildClasses;
23
	use HasMeta;
24
25
	/**
26
	 * @var bool resolve UUID relations automatically
27
	 */
28
	public $_autoResolveRelations = false;
29
30
	/**
31
	 * @var array list of field names containing relations to resolve
32
	 */
33
	public $_resolveRelations = [];
34
35
36
	/**
37
	 * @var array the path of nested components
38
	 */
39
	public $_componentPath = [];
40
41
42
	/**
43
	 * @var Collection all the fields for the Block
44
	 */
45
	private $_fields;
46
47
48
	/**
49
	 * @var Page|Block reference to the parent Block or Page
50
	 */
51
	private $_parent;
52
53
	/**
54
	 * Takes the Block’s content and a reference to the parent
55
	 * @param $content
56
	 * @param $parent
57
	 */
58
	public function __construct($content, $parent = null)
59
	{
60
		$this->_parent = $parent;
61
62
		$this->preprocess($content);
63
64
		$this->_componentPath = array_merge($parent->_componentPath, [Str::lower($this->meta()['component'])]);
65
66
		$this->processFields();
67
68
		// run automatic traits - methods matching initTraitClassName()
69
		foreach (class_uses_recursive($this) as $trait) {
70
			if (method_exists($this, $method = 'init' . class_basename($trait))) {
71
				$this->{$method}();
72
			}
73
		}
74
	}
75
76
	/**
77
	 * Returns the containing every field of content
78
	 *
79
	 * @return Collection
80
	 */
81
	public function content() {
82
		return $this->_fields;
83
	}
84
85
	/**
86
	 * Checks if the fields contain the specified key
87
	 *
88
	 * @param $key
89
	 * @return bool
90
	 */
91
	public function has($key) {
92
		return $this->_fields->has($key);
93
	}
94
95
	/**
96
	 * Returns the parent Block
97
	 *
98
	 * @return Block
99
	 */
100
	public function parent() {
101
		return $this->_parent;
102
	}
103
104
	/**
105
	 * Returns the page this Block belongs to
106
	 *
107
	 * @return Block
108
	 */
109
	public function page() {
110
		if ($this->parent() instanceof Page) {
0 ignored issues
show
introduced by
$this->parent() is never a sub-type of Riclep\Storyblok\Page.
Loading history...
111
			return $this->parent();
112
		}
113
114
		return $this->parent()->page();
115
	}
116
117
	/**
118
	 * Returns the first matching view, passing it the fields
119
	 *
120
	 * @return View
121
	 */
122
	public function render() {
123
		return view()->first($this->views(), ['block' => $this]);
124
	}
125
126
	/**
127
	 * Returns an array of possible views for the current Block based on
128
	 * it’s $componentPath match the component prefixed by each of it’s
129
	 * ancestors in turn, starting with the closest, for example:
130
	 *
131
	 * $componentPath = ['page', 'parent', 'child', 'this_block'];
132
	 *
133
	 * Becomes a list of possible views like so:
134
	 * ['child.this_block', 'parent.this_block', 'page.this_block'];
135
	 *
136
	 * Override this method with your custom implementation for
137
	 * ultimate control
138
	 *
139
	 * @return array
140
	 */
141
	public function views() {
142
		$compontentPath = $this->_componentPath;
143
		array_pop($compontentPath);
144
145
		$views = array_map(function($path) {
146
			return config('storyblok.view_path') . 'blocks.' . $path . '.' . $this->component();
147
		}, $compontentPath);
148
149
		$views = array_reverse($views);
150
151
		$views[] = config('storyblok.view_path') . 'blocks.' . $this->component();
152
153
		return $views;
154
	}
155
156
	/**
157
	 * Returns a component X generations previous
158
	 *
159
	 * @param $generation int
160
	 * @return mixed
161
	 */
162
	public function ancestorComponentName($generation)
163
	{
164
		return $this->_componentPath[count($this->_componentPath) - ($generation + 1)];
165
	}
166
167
	/**
168
	 * Checks if the current component is a child of another
169
	 *
170
	 * @param $parent string
171
	 * @return bool
172
	 */
173
	public function isChildOf($parent)
174
	{
175
		return $this->_componentPath[count($this->_componentPath) - 2] === $parent;
176
	}
177
178
	/**
179
	 * Checks if the component is an ancestor of another
180
	 *
181
	 * @param $parent string
182
	 * @return bool
183
	 */
184
	public function isAncestorOf($parent)
185
	{
186
		return in_array($parent, $this->parent()->_componentPath);
187
	}
188
189
	/**
190
	 * Returns the current Block’s component name from Storyblok
191
	 *
192
	 * @return string
193
	 */
194
	public function component() {
195
		return $this->_meta['component'];
196
	}
197
198
199
	/**
200
	 * Returns the HTML comment required for making this Block clickable in
201
	 * Storyblok’s visual editor. Don’t forget to set comments to true in
202
	 * your Vue.js app configuration.
203
	 *
204
	 * @return string
205
	 */
206
	public function editorLink() {
207
		if (array_key_exists('_editable', $this->_meta)) {
208
			return $this->_meta['_editable'];
209
		}
210
211
		return '';
212
	}
213
214
215
	/**
216
	 * Magic accessor to pull content from the _fields collection. Works just like
217
	 * Laravel’s model accessors. Matches public methods with the follow naming
218
	 * convention getSomeFieldAttribute() - called via $block->some_field
219
	 *
220
	 * @param $key
221
	 * @return bool|string
222
	 */
223
	public function __get($key) {
224
		$accessor = 'get' . Str::studly($key) . 'Attribute';
225
226
		if (method_exists($this, $accessor)) {
227
			return $this->$accessor();
228
		}
229
230
		try {
231
			if ($this->has($key)) {
232
				return $this->_fields[$key];
233
			}
234
		} catch (Exception $e) {
235
			return 'Caught exception: ' .  $e->getMessage();
236
		}
237
	}
238
239
	/**
240
	 * Loops over every field to get te ball rolling
241
	 */
242
	private function processFields() {
243
		$this->_fields->transform(function ($field, $key) {
244
			return $this->getFieldType($field, $key);
245
		});
246
	}
247
248
	/**
249
	 * Converts fields into Field Classes based on various properties of their content
250
	 *
251
	 * @param $field
252
	 * @param $key
253
	 * @return array|Collection|mixed|Asset|Image|MultiAsset|RichText|Table
254
	 */
255
	private function getFieldType($field, $key) {
256
		// TODO process old asset fields
257
		// TODO option to convert all text fields to a class - single or multiline?
258
259
		// does the Block assign any $casts? This is key (field) => value (class)
260
		if (property_exists($this, 'casts') && array_key_exists($key, $this->casts)) {
0 ignored issues
show
Bug introduced by
$this->casts of type boolean|string is incompatible with the type array expected by parameter $search of array_key_exists(). ( Ignorable by Annotation )

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

260
		if (property_exists($this, 'casts') && array_key_exists($key, /** @scrutinizer ignore-type */ $this->casts)) {
Loading history...
Bug Best Practice introduced by
The property casts does not exist on Riclep\Storyblok\Block. Since you implemented __get, consider adding a @property annotation.
Loading history...
261
			return new $this->casts[$key]($field, $this);
262
		}
263
264
		// find Fields specific to this Block matching: BlockNameFieldName
265
		if ($class = $this->getChildClassName('Field', $this->component() . '_' . $key)) {
266
			return new $class($field, $this);
267
		}
268
269
		// auto-match Field classes
270
		if ($class = $this->getChildClassName('Field', $key)) {
271
			return new $class($field, $this);
272
		}
273
274
		// single item relations
275
		if (Str::isUuid($field) && in_array($key, $this->_resolveRelations)) {
276
			$request = new RequestStory();
277
			$response = $request->get($field);
278
279
			$class = $this->getChildClassName('Block', $response['content']['component']);
280
			$relationClass = new $class($response['content'], $this);
281
282
			$relationClass->addMeta([
283
				'name' => $response['name'],
284
				'published_at' => $response['published_at'],
285
				'full_slug' => $response['full_slug'],
286
			]);
287
288
			return $relationClass;
289
		}
290
291
		// complex fields
292
		if (is_array($field) && !empty($field)) {
293
			return $this->arrayFieldTypes($field, $key);
294
		}
295
296
		// strings or anything else - do nothing
297
		return $field;
298
	}
299
300
301
	/**
302
	 * When the field is an array we need to do more processing
303
	 *
304
	 * @param $field
305
	 * @return Collection|mixed|Asset|Image|MultiAsset|RichText|Table
306
	 */
307
	private function arrayFieldTypes($field, $key) {
308
		// match link fields
309
		if (array_key_exists('linktype', $field)) {
310
			$class = 'Riclep\Storyblok\Fields\\' . Str::studly($field['linktype']) . 'Link';
311
312
			return new $class($field, $this);
313
		}
314
315
		// match rich-text fields
316
		if (array_key_exists('type', $field) && $field['type'] === 'doc') {
317
			return new RichText($field, $this);
318
		}
319
320
		// match asset fields - detecting raster images
321
		if (array_key_exists('fieldtype', $field) && $field['fieldtype'] === 'asset') {
322
			if (Str::endsWith($field['filename'], ['.jpg', '.jpeg', '.png', '.gif', '.webp'])) {
323
				return new Image($field, $this);
324
			}
325
326
			return new Asset($field, $this);
327
		}
328
329
		// match table fields
330
		if (array_key_exists('fieldtype', $field) && $field['fieldtype'] === 'table') {
331
			return new Table($field, $this);
332
		}
333
334
		// it’s an array of relations - request them if we’re auto resolving
335
		if (Str::isUuid($field[0])) {
336
			if ($this->_autoResolveRelations || in_array($key, $this->_resolveRelations)) {
337
				return collect($field)->transform(function ($relation) {
338
					$request = new RequestStory();
339
					$response = $request->get($relation);
340
341
					$class = $this->getChildClassName('Block', $response['content']['component']);
342
					$relationClass = new $class($response['content'], $this);
343
344
					$relationClass->addMeta([
345
						'name' => $response['name'],
346
						'published_at' => $response['published_at'],
347
						'full_slug' => $response['full_slug'],
348
					]);
349
350
					return $relationClass;
351
				});
352
			}
353
		}
354
355
		// has child items - single option, multi option and Blocks fields
356
		if (is_array($field[0])) {
357
			// resolved relationships - entire story is returned, we just want the content and a few meta items
358
			if (array_key_exists('content', $field[0])) {
359
				return collect($field)->transform(function ($relation) {
360
					$class = $this->getChildClassName('Block', $relation['content']['component']);
361
					$relationClass = new $class($relation['content'], $this);
362
363
					$relationClass->addMeta([
364
						'name' => $relation['name'],
365
						'published_at' => $relation['published_at'],
366
						'full_slug' => $relation['full_slug'],
367
					]);
368
369
					return $relationClass;
370
				});
371
			}
372
373
			// this field holds blocks!
374
			if (array_key_exists('component', $field[0])) {
375
				return collect($field)->transform(function ($block) {
376
					$class = $this->getChildClassName('Block', $block['component']);
377
378
					return new $class($block, $this);
379
				});
380
			}
381
382
			// multi assets
383
			if (array_key_exists('filename', $field[0])) {
384
				return new MultiAsset($field, $this);
385
			}
386
		}
387
388
		// just return the array
389
		return $field;
390
	}
391
392
	/**
393
	 * Storyblok returns fields and other meta content at the same level so
394
	 * let’s do a little tidying up first
395
	 *
396
	 * @param $content
397
	 */
398
	private function preprocess($content) {
399
		$this->_fields = collect(array_diff_key($content, array_flip(['_editable', '_uid', 'component'])));
400
401
		// remove non-content keys
402
		$this->_meta = array_intersect_key($content, array_flip(['_editable', '_uid', 'component']));
403
	}
404
405
	/**
406
	 * Returns cotent of the field. In the visual editor it returns a VueJS template tag
407
	 *
408
	 * @param $field
409
	 * @return string
410
	 */
411
	public function liveField($field) {
412
		if (config('storyblok.edit_mode')) {
413
			return '{{ Object.keys(laravelStoryblokLive).length ? laravelStoryblokLive.uuid_' . str_replace('-', '_', $this->uuid()) . '.' . $field . ' : null }}';
414
		}
415
416
		return $this->{$field};
417
	}
418
419
	/**
420
	 * Flattens all the fields in an array keyed by their UUID to make linking the JS simple
421
	 */
422
	public function flatten() {
423
		$this->content()->each(function ($item, $key) {
424
425
			if ($item instanceof Collection) {
426
				$item->each(function ($item) {
427
					$item->flatten();
428
				});
429
			} elseif ($item instanceof Field) {
430
				$this->page()->liveContent['uuid_' . str_replace('-', '_', $this->uuid())][$key] = (string) $item;
0 ignored issues
show
Bug Best Practice introduced by
The property liveContent does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
431
			} else {
432
				$this->page()->liveContent['uuid_' . str_replace('-', '_', $this->uuid())][$key] = $item;
433
			}
434
		});
435
	}
436
437
	/**
438
	 * Let’s up loop over the fields in Blade without needing to
439
	 * delve deep into the content collection
440
	 *
441
	 * @return \Traversable
442
	 */
443
	public function getIterator() {
444
		return $this->_fields;
445
	}
446
}