Completed
Push — master ( da7383...0076ce )
by Richard
09:03 queued 29s
created

Block::__construct()   A

Complexity

Conditions 6
Paths 24

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 16
c 1
b 0
f 0
nc 24
nop 2
dl 0
loc 31
rs 9.1111
1
<?php
2
3
// TODO - add defaults / null object
4
// TODO - date casting to Carbon
5
6
/////// blocks might be keyed with numbers from storyblok.
7
/// we might need to be able to access specific ones - reordering content will change the number
8
/// we either need a method to find a specific child (by component name)
9
/// or a Block Trait to key content by the child component name
10
11
namespace Riclep\Storyblok;
12
13
14
use Carbon\Carbon;
15
use Exception;
16
use Illuminate\Support\Collection;
17
use Illuminate\Support\Str;
18
use ReflectionClass;
19
use ReflectionMethod;
20
use Riclep\Storyblok\Traits\AutoParagraphs;
21
use Riclep\Storyblok\Traits\ConvertsMarkdown;
22
use Riclep\Storyblok\Traits\ConvertsRichtext;
23
use Riclep\Storyblok\Traits\ProcessesBlocks;
24
use Riclep\Storyblok\Traits\RequestsStories;
25
26
abstract class Block implements \JsonSerializable, \Iterator, \ArrayAccess, \Countable
27
{
28
	use ProcessesBlocks;
29
	use RequestsStories;
30
	use ConvertsMarkdown;
31
	use ConvertsRichtext;
32
	use AutoParagraphs;
33
34
	public $_meta;
35
36
	protected $_componentPath = [];
37
	protected $_uid;
38
	protected $component;
39
	protected $content;
40
41
	private $_editable;
42
	private $appends;
43
	private $excluded;
44
	private $fieldtype;
45
	private $iteratorIndex = 0;
46
	private $parent;
47
48
	/**
49
	 * Converts Storyblok’s API response into something usable by us. Each block becomes a class
50
	 * with the Storyblok UUID, the component name and any content under it’s own content key
51
	 *
52
	 * @param $block
53
	 * @param $parent
54
	 * @throws \ReflectionException
55
	 */
56
	public function __construct($block, $parent)
57
	{
58
		$this->parent = $parent;
59
60
		if (array_key_exists('content', $block)) {
61
			// child story so go straight to the contents but store a few useful meta items from the Story
62
			$this->processStoryblokKeys($block['content']);
63
			$this->_meta = array_intersect_key($block, array_flip(['name', 'created_at', 'published_at', 'slug', 'full_slug']));
64
		} else {
65
			$this->processStoryblokKeys($block);
66
		}
67
68
		$this->content->transform(function($item, $key) {
69
			return $this->processBlock($item, $key);
70
		});
71
72
		$this->carboniseDates();
73
74
		// run the used ‘automatic’ traits
75
		foreach (class_uses_recursive($this) as $trait) {
76
			if (method_exists($this, $method = 'init' . class_basename($trait))) {
77
				$this->{$method}();
78
			}
79
		}
80
81
		if ($this->getMethods()->contains('transform')) {
82
			$this->transform();
0 ignored issues
show
Bug introduced by
The method transform() does not exist on Riclep\Storyblok\Block. ( Ignorable by Annotation )

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

82
			$this->/** @scrutinizer ignore-call */ 
83
          transform();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
83
		}
84
85
		if (method_exists($this, 'init')) {
86
			$this->init();
87
		}
88
	}
89
90
	/**
91
	 * Tidies up and moves a few items from the JSON response into
92
	 * better places for our requirements
93
	 *
94
	 * @param $block
95
	 */
96
	private function processStoryblokKeys($block) {
97
		$this->_uid = $block['_uid'] ?? null;
98
		$this->component = $block['component'] ?? null;
99
		$this->content = collect(array_diff_key($block, array_flip(['_editable', '_uid', 'component', 'plugin', 'fieldtype'])));
100
		$this->fieldtype = $block['fieldtype'] ?? null;
101
	}
102
103
	/**
104
	 * Returns the HTML comment needed to link the visual editor to
105
	 * the content in the view
106
	 *
107
	 * @return string
108
	 */
109
	public function editableBridge()
110
	{
111
		return $this->_editable;
112
	}
113
114
	/**
115
	 * Returns a random item from the cotent. Useful when you want to get a random item
116
	 * from a collection to similar Blocks such as a random banner.
117
	 *
118
	 * @return mixed
119
	 */
120
	public function random()
121
	{
122
		return $this->content->random();
123
	}
124
125
	/**
126
	 * Return the content Collection
127
	 *
128
	 * @return mixed
129
	 */
130
	public function content()
131
	{
132
		return $this->content;
133
	}
134
135
	/**
136
	 * Returns the name of the component
137
	 *
138
	 * @return string
139
	 */
140
	public function component()
141
	{
142
		return $this->component;
143
	}
144
145
	/**
146
	 * Loops over all the components an gets an array of their names in the order
147
	 * that they have been nested
148
	 *
149
	 * @param $componentPath
150
	 */
151
	public function makeComponentPath($componentPath)
152
	{
153
		$componentPath[] = $this->component();
154
155
		$this->_componentPath = $componentPath;
156
157
		// loop over all child classes, pass down current component list
158
		$this->content->each(function($block) use ($componentPath) {
159
			if ($block instanceof Block || $block instanceof Asset) {
160
				$block->makeComponentPath($componentPath);
161
			}
162
		});
163
	}
164
165
	/**
166
	 * Returns the parent class
167
	 *
168
	 * @return mixed
169
	 */
170
	public function parent()
171
	{
172
		return $this->parent;
173
	}
174
175
	/**
176
	 * Gets the page that this Block is part of
177
	 *
178
	 * @return Page
179
	 */
180
	public function page() {
181
		if ($this->parent instanceof Page) {
182
			return $this->parent;
183
		}
184
185
		return $this->parent->page();
186
	}
187
188
	/**
189
	 * Checks if the component has particular children
190
	 *
191
	 * @param $componentName
192
	 * @return mixed
193
	 */
194
	public function hasChildComponent($componentName) {
195
		return $this->content->filter(function ($block) use ($componentName) {
196
			return $block->component === $componentName;
197
		});
198
	}
199
200
	/**
201
	 * Returns the component’s path
202
	 *
203
	 * @return array
204
	 */
205
	public function componentPath()
206
	{
207
		return $this->_componentPath;
208
	}
209
210
	/**
211
	 * Returns a component X generations previous
212
	 *
213
	 * @param $generation
214
	 * @return mixed
215
	 */
216
	public function getAncestorComponent($generation)
217
	{
218
		return $this->_componentPath[count($this->_componentPath) - ($generation + 1)];
219
	}
220
221
	/**
222
	 * Checks if the current component is a child of another
223
	 *
224
	 * @param $parent
225
	 * @return bool
226
	 */
227
	public function isChildOf($parent)
228
	{
229
		return $this->_componentPath[count($this->_componentPath) - 2] === $parent;
230
	}
231
232
	/**
233
	 * Checks if the component is an ancestor of another
234
	 *
235
	 * @param $parent
236
	 * @return bool
237
	 */
238
	public function isAncestorOf($parent)
239
	{
240
		return in_array($parent, $this->_componentPath);
241
	}
242
243
	/**
244
	 * Checks if the content contains the specified item
245
	 *
246
	 * @param string $key
247
	 * @return mixed
248
	 */
249
	public function has($key) {
250
		return $this->content->has($key);
251
	}
252
253
	/**
254
	 * Returns the UUID of the current component
255
	 *
256
	 * @return mixed
257
	 */
258
	public function uuid()
259
	{
260
		return $this->_uid;
261
	}
262
263
	/**
264
	 * Returns a rendered version of the block’s view
265
	 *
266
	 * @return string
267
	 */
268
	public function __toString()
269
	{
270
		return $this->content()->toJson(JSON_PRETTY_PRINT);
271
	}
272
273
	/**
274
	 * Determines how this object is converted to JSON
275
	 *
276
	 * @return mixed
277
	 */
278
	public function jsonSerialize()
279
	{
280
		if (property_exists($this, 'excluded')) {
281
			$content = $this->content->except($this->excluded);
282
		} else {
283
			$content = $this->content;
284
		}
285
286
		$attributes = [];
287
288
		// get the appended attributes
289
		if (property_exists($this, 'appends')) {
290
			foreach ($this->appends as $attribute) {
291
				$attributes[$attribute] = $this->{$attribute};
292
			}
293
		}
294
295
		return $content->merge(collect($attributes));
296
	}
297
298
299
	/**
300
	 * As all content sits under the content property we can ease access to these with a magic getter
301
	 * it looks inside the content collection for a matching key and returns it.
302
	 *
303
	 * If an accessor has been created for an existing or ‘new’ content item it will be returned.
304
	 *
305
	 * @param $name
306
	 * @return mixed
307
	 * @throws \ReflectionException
308
	 */
309
	public function __get($name) {
310
		$accessor = 'get' . Str::studly($name) . 'Attribute';
311
312
		if ($this->getMethods()->contains($accessor)) {
313
			return $this->$accessor();
314
		}
315
316
		try {
317
			if ($this->has($name)) {
318
				return $this->content[$name];
319
			}
320
321
			return false;
322
		} catch (Exception $e) {
323
			return 'Caught exception: ' .  $e->getMessage();
324
		}
325
	}
326
327
	/**
328
	 * Gets all the public methods for a class and it’s descendants
329
	 *
330
	 * @return Collection
331
	 * @throws \ReflectionException
332
	 */
333
	public function getMethods() {
334
		$class = new ReflectionClass($this);
335
		return collect($class->getMethods(ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED))->transform(function($item) {
336
			return $item->name;
337
		});
338
	}
339
340
	/**
341
	 * Converts date fields to carbon
342
	 */
343
	protected function carboniseDates() {
344
		$properties = get_object_vars($this);
345
346
		if (array_key_exists('dates', $properties)) {
347
			foreach ($properties['dates'] as $date) {
348
				if ($this->content->has($date)) {
349
					$this->content[$date] = Carbon::parse($this->content[$date]) ?: $this->content[$date];
350
				}
351
			}
352
		}
353
	}
354
355
	/**
356
	 * Do we have content
357
	 *
358
	 * @return mixed
359
	 */
360
	public function isEmpty() {
361
		return $this->content->isEmpty();
362
	}
363
364
	/*
365
	 * Methods for Iterator trait allowing us to foreach over a collection of
366
	 * Blocks and return their content. This makes accessing child content
367
	 * in Blade much cleaner
368
	 * */
369
	public function current()
370
	{
371
		return $this->content[$this->iteratorIndex];
372
	}
373
374
	public function next()
375
	{
376
		$this->iteratorIndex++;
377
	}
378
379
	public function rewind()
380
	{
381
		$this->iteratorIndex = 0;
382
	}
383
384
	public function key()
385
	{
386
		return $this->iteratorIndex;
387
	}
388
389
	public function valid()
390
	{
391
		return isset($this->content[$this->iteratorIndex]);
392
	}
393
394
	/*
395
	 * Methods for ArrayAccess Trait - allows us to dig straight down to the content collection
396
	 * when calling a key on the Object
397
	 * */
398
	public function offsetSet($offset, $value) {
399
		if (is_null($offset)) {
400
			$this->content[] = $value;
401
		} else {
402
			$this->content[$offset] = $value;
403
		}
404
	}
405
406
	public function offsetExists($offset) {
407
		return isset($this->content[$offset]);
408
	}
409
410
	public function offsetUnset($offset) {
411
		unset($this->content[$offset]);
412
	}
413
414
	public function offsetGet($offset) {
415
		return isset($this->content[$offset]) ? $this->content[$offset] : null;
416
	}
417
418
419
	/*
420
	 * Countable trait
421
	 * */
422
	public function count()
423
	{
424
		return $this->content->count();
425
	}
426
427
}