1 <?php
2
3 4 5 6 7 8 9 10
11
12 namespace Icybee\Modules\Views;
13
14 use ICanBoogie\ActiveRecord\Model;
15 use ICanBoogie\Debug;
16 use ICanBoogie\Event;
17 use ICanBoogie\Exception;
18 use ICanBoogie\I18n;
19 use ICanBoogie\HTTP\HTTPError;
20 use ICanBoogie\Module;
21 use ICanBoogie\Object;
22
23 use Brickrouge\Document;
24 use Brickrouge\Element;
25 use Brickrouge\Pager;
26
27 use BlueTihi\Context;
28
29 use Icybee\Modules\Nodes\Node;
30
31 32 33 34 35 36
37 class View extends Object
38 {
39 const ACCESS_CALLBACK = 'access_callback';
40 const ASSETS = 'assets';
41 const CLASSNAME = 'class';
42 const PROVIDER = 'provider';
43 const RENDERS = 'renders';
44 const RENDERS_ONE = 1;
45 const RENDERS_MANY = 2;
46 const RENDERS_OTHER = 3;
47 const TITLE = 'title';
48
49
50
51
52 protected $id;
53
54 protected function get_id()
55 {
56 return $this->id;
57 }
58
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
76 protected $renders;
77 protected $options;
78
79 protected function get_options()
80 {
81 return $this->options;
82 }
83
84 protected $engine;
85 protected $document;
86 protected $page;
87 protected $template;
88
89 protected $module;
90
91 protected function lazy_get_module()
92 {
93 global $core;
94
95 if (isset($this->module))
96 {
97 return $this->module;
98 }
99
100 return $core->modules[$this->module_id];
101 }
102
103 private $data;
104
105 protected function get_data()
106 {
107 return $this->data;
108 }
109
110 public $module_id;
111 public $type;
112
113 public function __construct($id, array $options, $engine, $document, $page, $template=null)
114 {
115 unset($this->module);
116
117 $this->options = $options;
118
119 $this->id = $id;
120 $this->type = $options['type'];
121 $this->module_id = $options['module'];
122 $this->renders = $options['renders'];
123
124 $this->engine = $engine;
125 $this->document = $document;
126 $this->page = $page;
127 $this->template = $template;
128 }
129
130 131 132 133 134
135 public function __invoke()
136 {
137 $this->validate_access();
138
139 $assets = array('css' => array(), 'js' => array());
140 $options = $this->options;
141
142 if (isset($options['assets']))
143 {
144 $assets = $options['assets'];
145 }
146
147 $this->add_assets($this->document, $assets);
148
149
150
151 try
152 {
153 $this->fire_render_before(array('id' => $this->id));
154
155 $rc = $this->render_outer_html();
156
157 $this->fire_render(array('id' => $this->id, 'rc' => &$rc));
158
159 return $rc;
160 }
161 catch (\Brickrouge\ElementIsEmpty $e)
162 {
163 return '';
164 }
165 }
166
167 168 169 170 171 172 173
174 protected function alter_context(Context $context)
175 {
176 $context['pagination'] = '';
177
178 if (isset($context['range']['limit']) && isset($context['range']['count']))
179 {
180 $range = $context['range'];
181
182 $context['pagination'] = new Pager
183 (
184 'div', array
185 (
186 Pager::T_COUNT => $range['count'],
187 Pager::T_LIMIT => $range['limit'],
188 Pager::T_POSITION => $range['page']
189 )
190 );
191 }
192
193 $context['view'] = $this;
194
195 return $context;
196 }
197
198 199 200 201 202 203
204 protected function add_assets(Document $document, array $assets=array())
205 {
206 if (isset($assets['js']))
207 {
208 foreach ((array) $assets['js'] as $asset)
209 {
210 list($file, $priority) = (array) $asset + array(1 => 0);
211
212 $document->js->add($file, $priority);
213 }
214 }
215
216 if (isset($assets['css']))
217 {
218 foreach ((array) $assets['css'] as $asset)
219 {
220 list($file, $priority) = (array) $asset + array(1 => 0);
221
222 $document->css->add($file, $priority);
223 }
224 }
225 }
226
227 228 229 230 231 232 233
234 protected function fire_render_before(array $params=array())
235 {
236 return new View\BeforeRenderEvent($this, $params);
237 }
238
239 240 241 242 243 244 245
246 protected function fire_render(array $params=array())
247 {
248 return new View\RenderEvent($this, $params);
249 }
250
251 252 253 254 255
256 protected function render_empty_inner_html()
257 {
258 global $core;
259
260 $html = null;
261 $default = I18n\t('The view %name is empty.', array('%name' => $this->id));
262 $type = $this->type;
263 $module = $this->module;
264 $module_flat_id = $module->flat_id;
265
266 if (isset($view['on_empty']))
267 {
268 $html = call_user_func($view['on_empty'], $this);
269 }
270 else if ($module)
271 {
272 $placeholder = $core->site->metas[$module_flat_id . ".{$this->type}.placeholder"];
273
274 if (!$placeholder)
275 {
276 $placeholder = $core->site->metas[$module_flat_id . '.placeholder'];
277 }
278
279 if ($placeholder)
280 {
281 $html = $placeholder;
282 }
283 else
284 {
285 $default = 'No record found.';
286 }
287
288 $default .= I18n\t
289 (
290 ' <ul><li>The placeholder %placeholder was tried, but it does not exists.</li><li>The %message was tried, but it does not exists.</li></ul>', array
291 (
292 'placeholder' => "$module_flat_id.$type.placeholder",
293 'message' => "$module_flat_id.$type.empty_view"
294 )
295 );
296 }
297
298 if (!$html)
299 {
300 $html = I18n\t('empty_view', array(), array('scope' => $module_flat_id . '.' . $type, 'default' => $default));
301
302 303 304 305 306 307
308 }
309
310 return $html;
311 }
312
313 314 315 316 317 318 319
320 protected function fire_render_empty_inner_html(array $payload=array())
321 {
322 return new View\RescueEvent($this, $payload);
323 }
324
325 protected function init_range()
326 {
327 global $core;
328
329 $limit_key = $this->module->flat_id . '.limits.' . $this->type;
330 $limit = $core->site->metas[$limit_key] ?: null;
331
332 return array
333 (
334 'page' => empty($_GET['page']) ? 0 : (int) $_GET['page'],
335 'limit' => $limit,
336 'count' => null
337 );
338 }
339
340 protected function provide($provider, &$context, array $conditions)
341 {
342 if (!class_exists($provider))
343 {
344 throw new \InvalidArgumentException(\ICanBoogie\format
345 (
346 'Provider class %class for view %id does not exists', array
347 (
348 'class' => $provider,
349 'id' => $this->id
350 )
351 ));
352 }
353
354 $provider = new $provider($this, $context, $this->module, $conditions, $this->renders);
355
356 return $provider();
357 }
358
359 360 361 362 363 364 365 366 367 368
369 protected function render_inner_html($template_path, $engine)
370 {
371 global $core;
372
373 $view = $this->options;
374 $bind = null;
375 $id = $this->id;
376 $page = $this->page;
377
378 if ($view['provider'])
379 {
380 list($constructor, $name) = explode('/', $id);
381
382 $this->range = $this->init_range();
383
384 $bind = $this->provide($this->options['provider'], $engine->context, $page->url_variables + $_GET);
385
386 $this->data = $bind;
387
388 $engine->context['this'] = $bind;
389 $engine->context['range'] = $this->range;
390
391 if (is_array($bind) && current($bind) instanceof Node)
392 {
393 new \BlueTihi\Context\LoadedNodesEvent($engine->context, $bind);
394 }
395 else if ($bind instanceof Node)
396 {
397 new \BlueTihi\Context\LoadedNodesEvent($engine->context, array($bind));
398 }
399 else if (!$bind)
400 {
401 $this->element->add_class('empty');
402
403 $html = (string) $this->render_empty_inner_html();
404
405 $this->fire_render_empty_inner_html
406 (
407 array
408 (
409 'html' => &$html
410 )
411 );
412
413 return $html;
414 }
415
416
417
418
419
420 if ($bind instanceof \Brickrouge\CSSClassNames)
421 {
422 $this->element['class'] .= ' ' . $bind->css_class;
423 }
424 }
425
426
427
428
429
430 $rc = '';
431
432 if (!$template_path)
433 {
434 throw new Exception('Unable to resolve template for view %id', array('id' => $id));
435 }
436
437 I18n::push_scope($this->module->flat_id);
438
439 try
440 {
441 $extension = pathinfo($template_path, PATHINFO_EXTENSION);
442
443 $module = $core->modules[$this->module_id];
444
445 $engine->context['core'] = $core;
446 $engine->context['document'] = $core->document;
447 $engine->context['page'] = $page;
448 $engine->context['module'] = $module;
449 $engine->context['view'] = $this;
450
451 $engine->context = $this->alter_context($engine->context);
452
453 if ('php' == $extension)
454 {
455 $rc = null;
456
457 ob_start();
458
459 try
460 {
461 $isolated_require = function ($__file__, $__exposed__)
462 {
463 extract($__exposed__);
464
465 require $__file__;
466 };
467
468 $isolated_require
469 (
470 $template_path, array
471 (
472 'bind' => $bind,
473 'context' => &$engine->context,
474 'core' => $core,
475 'document' => $core->document,
476 'page' => $page,
477 'module' => $module,
478 'view' => $this
479 )
480 );
481
482 $rc = ob_get_clean();
483 }
484 catch (\ICanBoogie\Exception\Config $e)
485 {
486 $rc = '<div class="alert">' . $e->getMessage() . '</div>';
487
488 ob_clean();
489 }
490 catch (\Exception $e)
491 {
492 ob_clean();
493
494 throw $e;
495 }
496 }
497 else if ('html' == $extension)
498 {
499 $template = file_get_contents($template_path);
500
501 if ($template === false)
502 {
503 throw new \Exception("Unable to read template from <q>$template_path</q>");
504 }
505
506 $rc = $engine($template, $bind, array('file' => $template_path));
507
508 if ($rc === null)
509 {
510 var_dump($template_path, file_get_contents($template_path), $rc);
511 }
512 }
513 else
514 {
515 throw new Exception('Unable to process file %file, unsupported type', array('file' => $template_path));
516 }
517 }
518 catch (\Exception $e)
519 {
520 I18n::pop_scope();
521
522 throw $e;
523 }
524
525 I18n::pop_scope();
526
527 return $rc;
528 }
529
530 protected $element;
531
532 protected function get_element()
533 {
534 return $this->element;
535 }
536
537 protected function alter_element(Element $element)
538 {
539 return $element;
540 }
541
542 543 544 545 546
547 protected function render_outer_html()
548 {
549 $class = '';
550 $type = \ICanBoogie\normalize($this->type);
551 $m = $this->module;
552
553 while ($m)
554 {
555 $normalized_id = \ICanBoogie\normalize($m->id);
556 $class = "view--$normalized_id--$type $class";
557
558 $m = $m->parent;
559 }
560
561 $this->element = new Element
562 (
563 'div', array
564 (
565 'id' => 'view-' . \ICanBoogie\normalize($this->id),
566 'class' => trim("view view--$type $class"),
567 'data-constructor' => $this->module->id
568 )
569 );
570
571 $this->element = $this->alter_element($this->element);
572
573
574
575 $template_path = $this->resolve_template_location();
576
577 $html = $this->render_inner_html($template_path, $this->engine);
578
579 if (preg_match('#\.html$#', $this->page->template))
580 {
581 if (Debug::$mode == Debug::MODE_DEV)
582 {
583
584 $possible_templates = implode(PHP_EOL, $this->template_resolver->templates);
585
586 $html = <<<EOT
587
588 <!-- Possible templates for view "{$this->id}":
589
590 $possible_templates
591
592 -->
593 $html
594 EOT;
595 }
596
597 $this->element[Element::INNER_HTML] = $html;
598
599 $html = (string) $this->element;
600 }
601
602 return $html;
603 }
604
605 606 607 608 609
610 protected function lazy_get_template_resolver()
611 {
612 return new TemplateResolver($this->id, $this->type, $this->module_id);
613 }
614
615 616 617 618 619 620 621 622 623
624 protected function resolve_template_location()
625 {
626 $resolver = $this->template_resolver;
627 $template = $resolver();
628
629 if (!$template)
630 {
631 throw new Exception
632 (
633 'Unable to resolve template for view %id. Tried: :list', array
634 (
635 'id' => $this->id,
636 ':list' => implode("\n<br />", $resolver->templates)
637 )
638 );
639 }
640
641 return $template;
642 }
643
644 645 646 647 648 649 650
651 protected function validate_access()
652 {
653 $access_callback = $this->options[self::ACCESS_CALLBACK];
654
655 if ($access_callback && !call_user_func($access_callback, $this))
656 {
657 throw new HTTPError
658 (
659 \ICanBoogie\format('The requested URL %uri requires authentication.', array
660 (
661 '%uri' => $_SERVER['REQUEST_URI']
662 )),
663
664 401
665 );
666 }
667
668 return true;
669 }
670 }
671
672 namespace Icybee\Modules\Views\View;
673
674 675 676
677 class BeforeRenderEvent extends \ICanBoogie\Event
678 {
679 public function __construct(\Icybee\Modules\Views\View $target, array $payload)
680 {
681 parent::__construct($target, 'render:before', $payload);
682 }
683 }
684
685 686 687
688 class RenderEvent extends \ICanBoogie\Event
689 {
690 public function __construct(\Icybee\Modules\Views\View $target, array $payload)
691 {
692 parent::__construct($target, 'render', $payload);
693 }
694 }
695
696 697 698
699 class RescueEvent extends \ICanBoogie\Event
700 {
701 702 703 704 705
706 public $html;
707
708 public function __construct(\Icybee\Modules\Views\View $target, array $payload)
709 {
710 parent::__construct($target, 'rescue', $payload);
711 }
712 }