Passed
Push — master ( 0076ce...72e11f )
by Richard
10:31 queued 15s
created

Block::jsonSerialize()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

248
		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...
249
			return new $this->casts[$key]($field, $this);
250
		}
251
252
		// find Fields specific to this Block matching: BlockNameFieldName
253
		if ($class = $this->getChildClassName('Field', $this->component() . '_' . $key)) {
254
			return new $class($field, $this);
255
		}
256
257
		// auto-match Field classes
258
		if ($class = $this->getChildClassName('Field', $key)) {
259
			return new $class($field, $this);
260
		}
261
262
		// complex fields
263
		if (is_array($field) && !empty($field)) {
264
			return $this->arrayFieldTypes($field);
265
		}
266
267
		// strings or anything else - do nothing
268
		return $field;
269
	}
270
271
272
	/**
273
	 * When the field is an array we need to do more processing
274
	 *
275
	 * @param $field
276
	 * @return Collection|mixed|Asset|Image|MultiAsset|RichText|Table
277
	 */
278
	private function arrayFieldTypes($field) {
279
		// match link fields
280
		if (array_key_exists('linktype', $field)) {
281
			$class = 'Riclep\Storyblok\Fields\\' . Str::studly($field['linktype']) . 'Link';
282
283
			return new $class($field, $this);
284
		}
285
286
		// match rich-text fields
287
		if (array_key_exists('type', $field) && $field['type'] === 'doc') {
288
			return new RichText($field, $this);
289
		}
290
291
		// match asset fields - detecting raster images
292
		if (array_key_exists('fieldtype', $field) && $field['fieldtype'] === 'asset') {
293
			if (Str::endsWith($field['filename'], ['.jpg', '.jpeg', '.png', '.gif', '.webp'])) {
294
				return new Image($field, $this);
295
			}
296
297
			return new Asset($field, $this);
298
		}
299
300
		// match table fields
301
		if (array_key_exists('fieldtype', $field) && $field['fieldtype'] === 'table') {
302
			return new Table($field, $this);
303
		}
304
305
		// it’s an array of relations - request them if we’re auto resolving
306
		if (Str::isUuid($field[0])) {
307
			if ($this->_autoResolveRelations) {
308
				return collect($field)->transform(function ($relation) {
309
					$request = new RequestStory();
310
					$response = $request->get($relation);
311
312
					$class = $this->getChildClassName('Block', $response['content']['component']);
313
					$relationClass = new $class($response['content'], $this);
314
315
					$relationClass->addMeta([
316
						'name' => $response['name'],
317
						'published_at' => $response['published_at'],
318
						'full_slug' => $response['full_slug'],
319
					]);
320
321
					return $relationClass;
322
				});
323
			}
324
		}
325
326
		// has child items - single option, multi option and Blocks fields
327
		if (is_array($field[0])) {
328
			// resolved relationships - entire story is returned, we just want the content and a few meta items
329
			if (array_key_exists('content', $field[0])) {
330
				return collect($field)->transform(function ($relation) {
331
					$class = $this->getChildClassName('Block', $relation['content']['component']);
332
					$relationClass = new $class($relation['content'], $this);
333
334
					$relationClass->addMeta([
335
						'name' => $relation['name'],
336
						'published_at' => $relation['published_at'],
337
						'full_slug' => $relation['full_slug'],
338
					]);
339
340
					return $relationClass;
341
				});
342
			}
343
344
			// this field holds blocks!
345
			if (array_key_exists('component', $field[0])) {
346
				return collect($field)->transform(function ($block) {
347
					$class = $this->getChildClassName('Block', $block['component']);
348
349
					return new $class($block, $this);
350
				});
351
			}
352
353
			// multi assets
354
			if (array_key_exists('filename', $field[0])) {
355
				return new MultiAsset($field, $this);
356
			}
357
		}
358
359
		// just return the array
360
		return $field;
361
	}
362
363
	/**
364
	 * Storyblok returns fields and other meta content at the same level so
365
	 * let’s do a little tidying up first
366
	 *
367
	 * @param $content
368
	 */
369
	private function preprocess($content) {
370
		$this->_fields = collect(array_diff_key($content, array_flip(['_editable', '_uid', 'component'])));
371
372
		// remove non-content keys
373
		$this->_meta = array_intersect_key($content, array_flip(['_editable', '_uid', 'component']));
374
	}
375
376
	/**
377
	 * Let’s up loop over the fields in Blade without needing to
378
	 * delve deep into the content collection
379
	 *
380
	 * @return \Traversable
381
	 */
382
	public function getIterator() {
383
		return $this->_fields;
384
	}
385
}