Passed
Push — master ( 328472...7b9327 )
by Richard
13:20 queued 12s
created

Block::jsonSerialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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