Passed
Push — master ( 37d5c2...da7383 )
by Richard
03:04 queued 12s
created

Block::content()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
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
47
	/**
48
	 * Converts Storyblok’s API response into something usable by us. Each block becomes a class
49
	 * with the Storyblok UUID, the component name and any content under it’s own content key
50
	 *
51
	 * @param $block
52
	 */
53
	public function __construct($block)
54
	{
55
		if (array_key_exists('content', $block)) {
56
			// child story so go straight to the contents but store a few useful meta items from the Story
57
			$this->processStoryblokKeys($block['content']);
58
			$this->_meta = array_intersect_key($block, array_flip(['name', 'created_at', 'published_at', 'slug', 'full_slug']));
59
		} else {
60
			$this->processStoryblokKeys($block);
61
		}
62
63
		$this->content->transform(function($item, $key) {
64
			return $this->processBlock($item, $key);
65
		});
66
67
		$this->carboniseDates();
68
69
		// run the used ‘automatic’ traits
70
		foreach (class_uses_recursive($this) as $trait) {
71
			if (method_exists($this, $method = 'init' . class_basename($trait))) {
72
				$this->{$method}();
73
			}
74
		}
75
76
		if ($this->getMethods()->contains('transform')) {
77
			$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

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