Completed
Push — 0.7 ( 5f9f60 )
by Olivier
01:41 queued 13s
created

View::assign()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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