View::set_content()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
rs 10
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
/*
4
 * This file is part of the ICanBoogie package.
5
 *
6
 * (c) Olivier Laviale <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace ICanBoogie\View;
13
14
use ICanBoogie\Accessor\AccessorTrait;
15
use ICanBoogie\EventCollectionProvider;
16
use ICanBoogie\OffsetNotDefined;
17
use ICanBoogie\PropertyNotDefined;
18
use ICanBoogie\Render\Renderer;
19
use ICanBoogie\Render\TemplateName;
20
use ICanBoogie\Render\TemplateNotFound;
21
use ICanBoogie\Routing\Controller;
22
23
/**
24
 * A view.
25
 *
26
 * @property-read Controller $controller The controller invoking the view.
27
 * @property-read Renderer $renderer The controller invoking the view.
28
 * @property-read array $variables The variables to pass to the template.
29
 * @property mixed $content The content of the view.
30
 * @property string $layout The name of the layout that should decorate the content.
31
 * @property string $template The name of the template that should render the content.
32
 * @property-read callable[] $layout_resolvers @internal
33
 * @property-read callable[] $template_resolvers @internal
34
 */
35
class View implements \ArrayAccess, \JsonSerializable
36
{
37
	use AccessorTrait;
38
39
	const TEMPLATE_TYPE_VIEW = 1;
40
	const TEMPLATE_TYPE_LAYOUT = 2;
41
	const TEMPLATE_TYPE_PARTIAL = 3;
42
43
	const TEMPLATE_PREFIX_VIEW = '';
44
	const TEMPLATE_PREFIX_LAYOUT = '@';
45
	const TEMPLATE_PREFIX_PARTIAL = '_';
46
47
	/**
48
	 * @var Controller
49
	 */
50
	private $controller;
51
52
	/**
53
	 * @return Controller
54
	 */
55
	protected function get_controller()
56
	{
57
		return $this->controller;
58
	}
59
60
	/**
61
	 * @var Renderer
62
	 */
63
	private $renderer;
64
65
	/**
66
	 * @return Renderer
67
	 */
68
	protected function get_renderer()
69
	{
70
		return $this->renderer;
71
	}
72
73
	/**
74
	 * View's variables.
75
	 *
76
	 * @var array
77
	 */
78
	private $variables = [];
79
80
	/**
81
	 * @see $variables
82
	 *
83
	 * @return array
84
	 */
85
	protected function get_variables()
86
	{
87
		return $this->variables;
88
	}
89
90
	/**
91
	 * @see $content
92
	 *
93
	 * @return mixed
94
	 */
95
	protected function get_content()
96
	{
97
		return isset($this->variables['content']) ? $this->variables['content'] : null;
98
	}
99
100
	/**
101
	 * @see $content
102
	 *
103
	 * @param mixed $content
104
	 */
105
	protected function set_content($content)
106
	{
107
		$this->variables['content'] = $content;
108
	}
109
110
	private $decorators = [];
111
112
	/**
113
	 * Return the name of the template.
114
	 *
115
	 * The template name is resolved as follows:
116
	 *
117
	 * - The `template` property of the route.
118
	 * - The `template` property of the controller.
119
	 * - The `{$controller->name}/{$controller->action}`, if the controller has an `action`
120
	 * property.
121
	 *
122
	 * @return string|null
123
	 */
124
	protected function lazy_get_template()
125
	{
126
		$controller = $this->controller;
127
128
		foreach ($this->template_resolvers as $provider)
129
		{
130
			try
131
			{
132
				return $provider($controller);
133
			}
134
			catch (PropertyNotDefined $e)
135
			{
136
				#
137
				# Resolver failed, we continue with the next.
138
				#
139
			}
140
141
		}
142
143
		return null;
144
	}
145
146
	/**
147
	 * Returns an array of callable used to resolve the {@link $template} property.
148
	 *
149
	 * @return callable[]
150
	 *
151
	 * @internal
152
	 */
153
	protected function get_template_resolvers()
154
	{
155
		return [
156
157
			function ($controller) {
158
159
				return $controller->route->template;
160
161
			},
162
163
			function ($controller) {
164
165
				return $controller->template;
166
167
			},
168
169
			function ($controller) {
170
171
				return $controller->name . "/" . $controller->action;
172
173
			}
174
175
		];
176
	}
177
178
	/**
179
	 * Returns the name of the layout.
180
	 *
181
	 * The layout name is resolved as follows:
182
	 *
183
	 * - The `layout` property of the route.
184
	 * - The `layout` property of the controller.
185
	 * - If the identifier of the route starts with "admin:", "admin" is returned.
186
	 * - If the route pattern is "/" and a "home" layout template is available, "home" is returned.
187
	 * - If the "@page" template is available, "page" is returned.
188
	 * - "default" is returned.
189
	 *
190
	 * @return string
191
	 */
192
	protected function lazy_get_layout()
193
	{
194
		$controller = $this->controller;
195
196
		foreach ($this->layout_resolvers as $resolver)
197
		{
198
			try
199
			{
200
				return $resolver($controller);
201
			}
202
			catch (PropertyNotDefined $e)
203
			{
204
				#
205
				# Resolver failed, we continue with the next.
206
				#
207
			}
208
209
		}
210
211
		if (strpos($controller->route->id, "admin:") === 0)
212
		{
213
			return 'admin';
214
		}
215
216
		if ($controller->route->pattern == "/" && $this->resolve_template('home', self::TEMPLATE_PREFIX_LAYOUT))
217
		{
218
			return 'home';
219
		}
220
221
		if ($this->resolve_template('page', self::TEMPLATE_PREFIX_LAYOUT))
222
		{
223
			return 'page';
224
		}
225
226
		return 'default';
227
	}
228
229
	/**
230
	 * Returns an array of callable used to resolve the {@link $template} property.
231
	 *
232
	 * @return callable[]
233
	 *
234
	 * @internal
235
	 */
236
	protected function get_layout_resolvers()
237
	{
238
		return [
239
240
			function ($controller) {
241
242
				return $controller->route->layout;
243
244
			},
245
246
			function ($controller) {
247
248
				return $controller->layout;
249
250
			}
251
252
		];
253
	}
254
255
	/**
256
	 * An event hook is attached to the `action` event of the controller for late rendering,
257
	 * which only happens if the response is `null`.
258
	 *
259
	 * @param Controller $controller The controller that invoked the view.
260
	 * @param Renderer $renderer
261
	 */
262
	public function __construct(Controller $controller, Renderer $renderer)
263
	{
264
		$this->controller = $controller;
265
		$this->renderer = $renderer;
266
		$this['view'] = $this;
267
268
		EventCollectionProvider::provide()->attach_to($controller, function (Controller\ActionEvent $event, Controller $target) {
269
270
			$this->on_action($event);
271
272
		});
273
	}
274
275
	/**
276
	 * @inheritdoc
277
	 *
278
	 * Returns an array with the following keys: `template`, `layout`, and `variables`.
279
	 */
280
	public function jsonSerialize()
281
	{
282
		return [
283
284
			'template' => $this->template,
285
			'layout' => $this->layout,
286
			'variables' => $this->ensure_without_this($this->variables)
287
288
		];
289
	}
290
291
	/**
292
	 * @inheritdoc
293
	 */
294
	public function offsetExists($offset)
295
	{
296
		return array_key_exists($offset, $this->variables);
297
	}
298
299
	/**
300
	 * @inheritdoc
301
	 *
302
	 * @throws OffsetNotDefined if the offset is not defined.
303
	 */
304
	public function offsetGet($offset)
305
	{
306
		if (!$this->offsetExists($offset))
307
		{
308
			throw new OffsetNotDefined([ $offset, $this ]);
309
		}
310
311
		return $this->variables[$offset];
312
	}
313
314
	/**
315
	 * @inheritdoc
316
	 */
317
	public function offsetSet($offset, $value)
318
	{
319
		$this->variables[$offset] = $value;
320
	}
321
322
	/**
323
	 * @inheritdoc
324
	 */
325
	public function offsetUnset($offset)
326
	{
327
		unset($this->variables[$offset]);
328
	}
329
330
	/**
331
	 * Assign multiple variables.
332
	 *
333
	 * @param array $variables
334
	 *
335
	 * @return $this
336
	 */
337
	public function assign(array $variables)
338
	{
339
		$this->variables = array_merge($this->variables, $variables);
340
341
		return $this;
342
	}
343
344
	/**
345
	 * Resolve a template pathname from its name and type.
346
	 *
347
	 * @param string $name Name of the template.
348
	 * @param string $prefix Template prefix.
349
	 * @param array $tried Reference to an array where tried paths are collected.
350
	 *
351
	 * @return string|false
352
	 */
353
	protected function resolve_template($name, $prefix, &$tried = [])
354
	{
355
		$tried = $tried ?: [];
356
357
		if ($prefix)
358
		{
359
			$name = TemplateName::from($name)->with_prefix($prefix);
360
		}
361
362
		try
363
		{
364
			return $this->renderer->resolve_template($name);
365
		}
366
		catch (TemplateNotFound $e)
367
		{
368
			$tried = $e->tried;
369
370
			return null;
371
		}
372
	}
373
374
	/**
375
	 * Add a template to decorate the content with.
376
	 *
377
	 * @param string $template Name of the template.
378
	 */
379
	public function decorate_with($template)
380
	{
381
		$this->decorators[] = $template;
382
	}
383
384
	/**
385
	 * Decorate the content.
386
	 *
387
	 * @param mixed $content The content to decorate.
388
	 *
389
	 * @return string
390
	 */
391
	protected function decorate($content)
392
	{
393
		$decorators = array_reverse($this->decorators);
394
395
		foreach ($decorators as $template)
396
		{
397
			$content = $this->renderer->render([
398
399
				Renderer::OPTION_CONTENT => $content,
400
				Renderer::OPTION_LAYOUT => $template
401
402
			]);
403
		}
404
405
		return $content;
406
	}
407
408
	/**
409
	 * Render the view.
410
	 *
411
	 * @return string
412
	 */
413
	public function render()
414
	{
415
		return $this->decorate($this->renderer->render([
416
417
			Renderer::OPTION_CONTENT => $this->content,
418
			Renderer::OPTION_TEMPLATE => $this->template,
419
			Renderer::OPTION_LAYOUT => $this->layout,
420
			Renderer::OPTION_LOCALS => $this->variables
421
422
		]));
423
	}
424
425
	/**
426
	 * Render a partial.
427
	 *
428
	 * @param string $template
429
	 * @param array $locals
430
	 * @param array $options
431
	 *
432
	 * @return string
433
	 */
434
	public function partial($template, array $locals = [], array $options = [])
435
	{
436
		return $this->renderer->render([
437
438
			Renderer::OPTION_PARTIAL => $template,
439
			Renderer::OPTION_LOCALS => $locals
440
441
		], $options);
442
	}
443
444
	/**
445
	 * Renders the view on `Controller::action` event.
446
	 *
447
	 * **Note:** The view is not rendered if the event's response is defined, which is the case
448
	 * when the controller obtained a result after its execution.
449
	 *
450
	 * @param Controller\ActionEvent $event
451
	 */
452
	protected function on_action(Controller\ActionEvent $event)
453
	{
454
		if ($event->result !== null)
455
		{
456
			return;
457
		}
458
459
		new View\BeforeRenderEvent($this, $event->result);
460
461
		if ($event->result !== null)
462
		{
463
			return;
464
		}
465
466
		$event->result = $this->render();
467
	}
468
469
	/**
470
	 * Ensures the array does not include our instance.
471
	 *
472
	 * @param array $array
473
	 *
474
	 * @return array
475
	 */
476
	private function ensure_without_this(array $array)
477
	{
478
		foreach ($array as $key => $value)
479
		{
480
			if ($value === $this)
481
			{
482
				unset($array[$key]);
483
			}
484
		}
485
486
		return $array;
487
	}
488
}
489