Passed
Push — master ( a5fc0a...e954fa )
by Richard
14:40 queued 12s
created

Block::hasChildBlock()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

332
		$this->_fields = collect(/** @scrutinizer ignore-type */ array_diff_key($content, array_flip($fields)));
Loading history...
333
334
		// remove non-content keys
335
		$this->_meta = array_intersect_key($content, array_flip($fields));
336
	}
337
338
	/**
339
	 * Casting Block to JSON
340
	 *
341
	 * @return Collection|mixed
342
	 */
343
	public function jsonSerialize(): mixed
344
	{
345
		return $this->content();
346
	}
347
348
	/**
349
	 * Let’s up loop over the fields in Blade without needing to
350
	 * delve deep into the content collection
351
	 *
352
	 * @return \Traversable
353
	 */
354
	public function getIterator(): \Traversable {
355
		return $this->_fields;
356
	}
357
358
	public function getRelation(RequestStory $request, $relation) {
359
		try {
360
			$response = $request->get($relation);
361
362
			$class = $this->getChildClassName('Block', $response['content']['component']);
363
			$relationClass = new $class($response['content'], $this);
364
365
			$relationClass->addMeta([
366
				'name' => $response['name'],
367
				'published_at' => $response['published_at'],
368
				'full_slug' => $response['full_slug'],
369
			]);
370
371
			return $relationClass;
372
		} catch (ApiException $e) {
373
			return null;
374
		}
375
	}
376
377
	public function getCasts() {
378
		return $this->_casts;
379
	}
380
}