Passed
Push — master ( 664f9d...666c7a )
by Richard
15:00 queued 11s
created

Block::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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