Passed
Push — master ( 4d1571...e1bca8 )
by Richard
03:28 queued 13s
created

Block::inverseRelation()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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

334
		$this->_fields = collect(/** @scrutinizer ignore-type */ array_diff_key($content, array_flip($fields)));
Loading history...
335
336
		// remove non-content keys
337
		$this->_meta = array_intersect_key($content, array_flip($fields));
338
	}
339
340
	/**
341
	 * Casting Block to JSON
342
	 *
343
	 * @return Collection|mixed
344
	 */
345
	public function jsonSerialize(): mixed
346
	{
347
		return $this->content();
348
	}
349
350
	/**
351
	 * Let’s up loop over the fields in Blade without needing to
352
	 * delve deep into the content collection
353
	 *
354
	 * @return \Traversable
355
	 */
356
	public function getIterator(): \Traversable {
357
		return $this->_fields;
358
	}
359
360
	public function getRelation(RequestStory $request, $relation) {
361
		try {
362
			$response = $request->get($relation);
363
364
			$class = $this->getChildClassName('Block', $response['content']['component']);
365
			$relationClass = new $class($response['content'], $this);
366
367
			$relationClass->addMeta([
368
				'name' => $response['name'],
369
				'published_at' => $response['published_at'],
370
				'full_slug' => $response['full_slug'],
371
				'page_uuid' => $relation,
372
			]);
373
374
			return $relationClass;
375
		} catch (ApiException $e) {
376
			return null;
377
		}
378
	}
379
380
	/**
381
	 * Returns an inverse relationship to the current Block. For example if Service has a Multi-Option field
382
	 * relationship to People, on People you can request all the Services it has been related to
383
	 *
384
	 * @param $foreignRelationshipField
385
	 * @param $foreignRelationshipType
386
	 * @param $components
387
	 * @param $options
388
	 * @return array
389
	 */
390
	public function inverseRelation($foreignRelationshipField, $foreignRelationshipType = 'multi', $components = null, $options = null) {
391
		$storyblokClient = resolve('Storyblok\Client');
392
393
		$type = 'any_in_array';
394
395
		if ($foreignRelationshipType === 'single') {
396
			$type = 'in';
397
		}
398
399
		$query = [
400
			'filter_query' => [
401
				$foreignRelationshipField => [$type => $this->meta('page_uuid') ?? $this->page()->uuid()]
402
			],
403
		];
404
405
		if ($components) {
406
			$query = array_merge_recursive($query, [
407
				'filter_query' => [
408
					'component' => ['in' => $components],
409
				]
410
			]);
411
		}
412
413
		if ($options) {
414
			$query = array_merge_recursive($query, $options);
415
		}
416
417
		$storyblokClient->getStories($query);
418
419
		return [
420
			'headers' => $storyblokClient->getHeaders(),
421
			'stories' => $storyblokClient->getBody()['stories'],
422
		];
423
	}
424
425
	public function getCasts() {
426
		return $this->_casts;
427
	}
428
}