Passed
Push — master ( 3242af...43b283 )
by Richard
17:32 queued 10s
created

Block::parent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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