Passed
Push — master ( c443bf...afef79 )
by Richard
03:53 queued 10s
created

Block::render()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 2
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
4
namespace Riclep\Storyblok;
5
6
use Illuminate\Support\Collection;
7
use Illuminate\Support\Str;
8
use Illuminate\View\View;
9
use Riclep\Storyblok\Fields\Asset;
10
use Riclep\Storyblok\Fields\Image;
11
use Riclep\Storyblok\Fields\MultiAsset;
12
use Riclep\Storyblok\Fields\RichText;
13
use Riclep\Storyblok\Fields\Table;
14
use Riclep\Storyblok\Traits\CssClasses;
15
use Riclep\Storyblok\Traits\HasChildClasses;
16
use Riclep\Storyblok\Traits\HasMeta;
17
18
class Block implements \IteratorAggregate
19
{
20
	use CssClasses;
21
	use HasChildClasses;
22
	use HasMeta;
23
24
	/**
25
	 * @var bool resolve UUID relations automatically
26
	 */
27
	public $_autoResolveRelations = false;
28
29
	/**
30
	 * @var array list of field names containing relations to resolve
31
	 */
32
	public $_resolveRelations = [];
33
34
	/**
35
	 * @var array the path of nested components
36
	 */
37
	public $_componentPath = [];
38
39
	/**
40
	 * @var array the path of nested components
41
	 */
42
	protected $_casts = [];
43
44
	/**
45
	 * @var Collection all the fields for the Block
46
	 */
47
	private $_fields;
48
49
	/**
50
	 * @var Page|Block reference to the parent Block or Page
51
	 */
52
	private $_parent;
53
54
	/**
55
	 * Takes the Block’s content and a reference to the parent
56
	 * @param $content
57
	 * @param $parent
58
	 */
59
	public function __construct($content, $parent = null)
60
	{
61
		$this->_parent = $parent;
62
63
		$this->preprocess($content);
64
65
		$this->_componentPath = array_merge($parent->_componentPath, [Str::lower($this->meta()['component'])]);
66
67
		$this->processFields();
68
69
		// run automatic traits - methods matching initTraitClassName()
70
		foreach (class_uses_recursive($this) as $trait) {
71
			if (method_exists($this, $method = 'init' . class_basename($trait))) {
72
				$this->{$method}();
73
			}
74
		}
75
	}
76
77
	/**
78
	 * Returns the containing every field of content
79
	 *
80
	 * @return Collection
81
	 */
82
	public function content() {
83
		return $this->_fields;
84
	}
85
86
	/**
87
	 * Checks if the fields contain the specified key
88
	 *
89
	 * @param $key
90
	 * @return bool
91
	 */
92
	public function has($key) {
93
		return $this->_fields->has($key);
94
	}
95
96
	/**
97
	 * Returns the parent Block
98
	 *
99
	 * @return Block
100
	 */
101
	public function parent() {
102
		return $this->_parent;
103
	}
104
105
	/**
106
	 * Returns the page this Block belongs to
107
	 *
108
	 * @return Block
109
	 */
110
	public function page() {
111
		if ($this->parent() instanceof Page) {
0 ignored issues
show
introduced by
$this->parent() is never a sub-type of Riclep\Storyblok\Page.
Loading history...
112
			return $this->parent();
113
		}
114
115
		return $this->parent()->page();
116
	}
117
118
	/**
119
	 * Returns the first matching view, passing it the fields
120
	 *
121
	 * @return View
122
	 */
123
	public function render() {
124
		return view()->first($this->views(), ['block' => $this]);
125
	}
126
127
	/**
128
	 * Returns an array of possible views for the current Block based on
129
	 * it’s $componentPath match the component prefixed by each of it’s
130
	 * ancestors in turn, starting with the closest, for example:
131
	 *
132
	 * $componentPath = ['page', 'parent', 'child', 'this_block'];
133
	 *
134
	 * Becomes a list of possible views like so:
135
	 * ['child.this_block', 'parent.this_block', 'page.this_block'];
136
	 *
137
	 * Override this method with your custom implementation for
138
	 * ultimate control
139
	 *
140
	 * @return array
141
	 */
142
	public function views() {
143
		$compontentPath = $this->_componentPath;
144
		array_pop($compontentPath);
145
146
		$views = array_map(function($path) {
147
			return config('storyblok.view_path') . 'blocks.' . $path . '.' . $this->component();
148
		}, $compontentPath);
149
150
		$views = array_reverse($views);
151
152
		$views[] = config('storyblok.view_path') . 'blocks.' . $this->component();
153
154
		return $views;
155
	}
156
157
	/**
158
	 * Returns a component X generations previous
159
	 *
160
	 * @param $generation int
161
	 * @return mixed
162
	 */
163
	public function ancestorComponentName($generation)
164
	{
165
		return $this->_componentPath[count($this->_componentPath) - ($generation + 1)];
166
	}
167
168
	/**
169
	 * Checks if the current component is a child of another
170
	 *
171
	 * @param $parent string
172
	 * @return bool
173
	 */
174
	public function isChildOf($parent)
175
	{
176
		return $this->_componentPath[count($this->_componentPath) - 2] === $parent;
177
	}
178
179
	/**
180
	 * Checks if the component is an ancestor of another
181
	 *
182
	 * @param $parent string
183
	 * @return bool
184
	 */
185
	public function isAncestorOf($parent)
186
	{
187
		return in_array($parent, $this->parent()->_componentPath);
188
	}
189
190
	/**
191
	 * Returns the current Block’s component name from Storyblok
192
	 *
193
	 * @return string
194
	 */
195
	public function component() {
196
		return $this->_meta['component'];
197
	}
198
199
200
	/**
201
	 * Returns the HTML comment required for making this Block clickable in
202
	 * Storyblok’s visual editor. Don’t forget to set comments to true in
203
	 * your Vue.js app configuration.
204
	 *
205
	 * @return string
206
	 */
207
	public function editorLink() {
208
		if (array_key_exists('_editable', $this->_meta) && config('storyblok.edit_mode')) {
209
			return $this->_meta['_editable'];
210
		}
211
212
		return '';
213
	}
214
215
216
	/**
217
	 * Magic accessor to pull content from the _fields collection. Works just like
218
	 * Laravel’s model accessors. Matches public methods with the follow naming
219
	 * convention getSomeFieldAttribute() - called via $block->some_field
220
	 *
221
	 * @param $key
222
	 * @return null|string
223
	 */
224
	public function __get($key) {
225
		$accessor = 'get' . Str::studly($key) . 'Attribute';
226
227
		if (method_exists($this, $accessor)) {
228
			return $this->$accessor();
229
		}
230
231
		if ($this->has($key)) {
232
			return $this->_fields[$key];
233
		}
234
235
		return null;
236
	}
237
238
	/**
239
	 * Loops over every field to get te ball rolling
240
	 */
241
	private function processFields() {
242
		$this->_fields->transform(function ($field, $key) {
243
			return $this->getFieldType($field, $key);
244
		});
245
	}
246
247
	/**
248
	 * Converts fields into Field Classes based on various properties of their content
249
	 *
250
	 * @param $field
251
	 * @param $key
252
	 * @return array|Collection|mixed|Asset|Image|MultiAsset|RichText|Table
253
	 * @throws \Storyblok\ApiException
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)) {
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) && ($this->_autoResolveRelations || in_array($key, $this->_resolveRelations))) {
276
			return $this->getRelation(new RequestStory(), $field);
277
		}
278
279
		// complex fields
280
		if (is_array($field) && !empty($field)) {
281
			return $this->arrayFieldTypes($field, $key);
282
		}
283
284
		// legacy image fields
285
		if (is_string($field) && Str::endsWith($field, ['.jpg', '.jpeg', '.png', '.gif', '.webp'])) {
286
			return new Image($field, $this);
287
		}
288
289
		// strings or anything else - do nothing
290
		return $field;
291
	}
292
293
294
	/**
295
	 * When the field is an array we need to do more processing
296
	 *
297
	 * @param $field
298
	 * @return Collection|mixed|Asset|Image|MultiAsset|RichText|Table
299
	 */
300
	private function arrayFieldTypes($field, $key) {
301
		// match link fields
302
		if (array_key_exists('linktype', $field)) {
303
			$class = 'Riclep\Storyblok\Fields\\' . Str::studly($field['linktype']) . 'Link';
304
305
			return new $class($field, $this);
306
		}
307
308
		// match rich-text fields
309
		if (array_key_exists('type', $field) && $field['type'] === 'doc') {
310
			return new RichText($field, $this);
311
		}
312
313
		// match asset fields - detecting raster images
314
		if (array_key_exists('fieldtype', $field) && $field['fieldtype'] === 'asset') {
315
			if (Str::endsWith($field['filename'], ['.jpg', '.jpeg', '.png', '.gif', '.webp'])) {
316
				return new Image($field, $this);
317
			}
318
319
			return new Asset($field, $this);
320
		}
321
322
		// match table fields
323
		if (array_key_exists('fieldtype', $field) && $field['fieldtype'] === 'table') {
324
			return new Table($field, $this);
325
		}
326
327
		if (array_key_exists(0, $field)) {
328
			// it’s an array of relations - request them if we’re auto or manual resolving
329
			if (Str::isUuid($field[0])) {
330
				if ($this->_autoResolveRelations || in_array($key, $this->_resolveRelations)) {
331
					return collect($field)->transform(function ($relation) {
332
						return $this->getRelation(new RequestStory(), $relation);
333
					});
334
				}
335
			}
336
337
			// has child items - single option, multi option and Blocks fields
338
			if (is_array($field[0])) {
339
				// resolved relationships - entire story is returned, we just want the content and a few meta items
340
				if (array_key_exists('content', $field[0])) {
341
					return collect($field)->transform(function ($relation) {
342
						$class = $this->getChildClassName('Block', $relation['content']['component']);
343
						$relationClass = new $class($relation['content'], $this);
344
345
						$relationClass->addMeta([
346
							'name' => $relation['name'],
347
							'published_at' => $relation['published_at'],
348
							'full_slug' => $relation['full_slug'],
349
						]);
350
351
						return $relationClass;
352
					});
353
				}
354
355
				// this field holds blocks!
356
				if (array_key_exists('component', $field[0])) {
357
					return collect($field)->transform(function ($block) {
358
						$class = $this->getChildClassName('Block', $block['component']);
359
360
						return new $class($block, $this);
361
					});
362
				}
363
364
				// multi assets
365
				if (array_key_exists('filename', $field[0])) {
366
					return new MultiAsset($field, $this);
367
				}
368
			}
369
		}
370
371
		// just return the array
372
		return $field;
373
	}
374
375
	/**
376
	 * Storyblok returns fields and other meta content at the same level so
377
	 * let’s do a little tidying up first
378
	 *
379
	 * @param $content
380
	 */
381
	private function preprocess($content) {
382
		$this->_fields = collect(array_diff_key($content, array_flip(['_editable', '_uid', 'component'])));
383
384
		// remove non-content keys
385
		$this->_meta = array_intersect_key($content, array_flip(['_editable', '_uid', 'component']));
386
	}
387
388
	/**
389
	 * Returns cotent of the field. In the visual editor it returns a VueJS template tag
390
	 *
391
	 * @param $field
392
	 * @return string
393
	 */
394
	public function liveField($field) {
395
		if (config('storyblok.edit_mode')) {
396
			return '{{ Object.keys(laravelStoryblokLive).length ? laravelStoryblokLive.uuid_' . str_replace('-', '_', $this->uuid()) . '.' . $field . ' : null }}';
397
		}
398
399
		return $this->{$field};
400
	}
401
402
	/**
403
	 * Flattens all the fields in an array keyed by their UUID to make linking the JS simple
404
	 */
405
	public function flatten() {
406
		$this->content()->each(function ($item, $key) {
407
408
			if ($item instanceof Collection) {
409
				$item->each(function ($item) {
410
					$item->flatten();
411
				});
412
			} elseif ($item instanceof Field) {
413
				$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...
414
			} else {
415
				$this->page()->liveContent['uuid_' . str_replace('-', '_', $this->uuid())][$key] = $item;
416
			}
417
		});
418
	}
419
420
	/**
421
	 * Let’s up loop over the fields in Blade without needing to
422
	 * delve deep into the content collection
423
	 *
424
	 * @return \Traversable
425
	 */
426
	public function getIterator() {
427
		return $this->_fields;
428
	}
429
430
	protected function getRelation(RequestStory $request, $relation) {
431
		$response = $request->get($relation);
432
433
		$class = $this->getChildClassName('Block', $response['content']['component']);
434
		$relationClass = new $class($response['content'], $this);
435
436
		$relationClass->addMeta([
437
			'name' => $response['name'],
438
			'published_at' => $response['published_at'],
439
			'full_slug' => $response['full_slug'],
440
		]);
441
442
		return $relationClass;
443
	}
444
}