<?php /** * View renderer * @property-read string $_content * @property-read string|null $_trash */ class Loco_mvc_View implements IteratorAggregate { /** * @var Loco_mvc_ViewParams */ private $scope; /** * View that is decorating current view * @var self */ private $parent; /** * Current template as full path to PHP file * @var string */ private $template; /** * Current working directory for finding templates by relative path * @var string */ private $cwd; /** * Name of current output buffer * @var string */ private $block; /** * @internal */ public function __construct( array $args = [] ){ $this->scope = new Loco_mvc_ViewParams( $args ); $this->cwd = loco_plugin_root().'/tpl'; } /** * Change base path for template paths * @param string $path relative to current directory * @return Loco_mvc_View */ public function cd( $path ){ if( $path && '/' === substr($path,0,1) ){ $this->cwd = untrailingslashit( loco_plugin_root().'/tpl'.$path ); } else { $this->cwd = untrailingslashit( $this->cwd.'/'.$path ); } return $this; } /** * @internal * Clean up if something abruptly stopped rendering before graceful end */ public function __destruct(){ if( $this->block ){ ob_end_clean(); } } /** * Render error screen HTML * @return string */ public static function renderError( Loco_error_Exception $e ){ $view = new Loco_mvc_View; try { $view->set( 'error', $e ); return $view->render( $e->getTemplate() ); } catch( Exception $e ){ return '<h1>'.esc_html( $e->getMessage() ).'</h1>'; } } /** * Make this view a child of another template. i.e. decorate this with that. * Parent will have access to original argument scope, but separate from now on * @param string $tpl * @return self the parent view */ private function extend( $tpl ){ $this->parent = new Loco_mvc_View; $this->parent->cwd = $this->cwd; $this->parent->setTemplate( $tpl ); return $this->parent; } /** * After start is called any captured output will be placed in the named variable * @param string $name * @return void */ private function start( $name ){ $this->stop(); $this->scope[$name] = null; $this->block = $name; } /** * When stop is called, buffered output is saved into current variable for output by parent template, or at end of script. * @return void */ private function stop(){ $content = ob_get_contents(); ob_clean(); if( $b = $this->block ){ if( isset($this->scope[$b]) ){ $content = $this->scope[$b].$content; } $this->scope[$b] = new _LocoViewBuffer($content); } $this->block = '_trash'; } /** * {@inheritDoc} */ #[ReturnTypeWillChange] public function getIterator(){ return $this->scope; } /** * @internal * @param string $prop * @return mixed */ public function __get( $prop ){ return $this->has($prop) ? $this->get($prop) : null; } /** * @param string $prop * @return bool */ public function has( $prop ){ return $this->scope->offsetExists($prop); } /** * Get property after checking with self::has * @param string $prop * @return mixed */ public function get( $prop ){ return $this->scope[$prop]; } /** * Set a view argument * @param string $prop * @param mixed $value * @return self */ public function set( $prop, $value ){ $this->scope[$prop] = $value; return $this; } /** * Main entry to rendering complete template * @param string $tpl template name excluding extension * @param array|null $args extra arguments to set in view scope * @param self|null $parent parent view rendering this view * @return string */ public function render( $tpl, array $args = null, Loco_mvc_View $parent = null ){ if( $this->block ){ return $this->fork()->render( $tpl, $args, $this ); } $this->setTemplate($tpl); if( $parent && $this->template === $parent->template ){ throw new Loco_error_Exception('Avoiding infinite loop'); } if( is_array($args) ){ foreach( $args as $prop => $value ){ $this->set($prop, $value); } } ob_start(); $content = $this->buffer(); ob_end_clean(); return $content; } /** * Do actual render of currently validated template path * @return string content not captured in sub-blocks */ private function buffer(){ $this->start('_trash'); $this->execTemplate( $this->template ); $this->stop(); $this->block = null; // decorate via parent view if there is one if( $this->parent ){ $this->parent->scope = clone $this->scope; $this->parent->set('_content', $this->_trash ); return $this->parent->buffer(); } // else at the root of view chain return (string) $this->_trash; } /** * Set current template * @param string $tpl Path to template, excluding file extension */ public function setTemplate( $tpl ){ $file = new Loco_fs_File( $tpl.'.php' ); $file->normalize( $this->cwd ); if( ! $file->exists() ){ $debug = str_replace( loco_plugin_root().'/', '', $file->getPath() ); throw new Loco_error_Exception( 'Template not found: '.$debug ); } $this->cwd = $file->dirname(); $this->template = $file->getPath(); } /** * @return Loco_mvc_View */ private function fork(){ $view = new Loco_mvc_View; $view->cwd = $this->cwd; $view->scope = clone $this->scope; return $view; } /** * Do actual runtime template include * @param string $template * @return void */ private function execTemplate( $template ){ $params = $this->scope; extract( $params->getArrayCopy() ); include $template; } /** * Link generator * @param string $route page route, e.g. "config" * @param array $args optional page arguments * @return Loco_mvc_ViewParams */ public function route( $route, array $args = [] ){ return new Loco_mvc_ViewParams( [ 'href' => Loco_mvc_AdminRouter::generate( $route, $args ), ] ); } /** * Shorthand for `echo esc_html( sprintf( ...` * @param string $text * @return string */ private static function e( $text ){ if( 1 < func_num_args() ){ $args = func_get_args(); $text = call_user_func_array( 'sprintf', $args ); } echo htmlspecialchars( $text, ENT_COMPAT, 'UTF-8' ); return ''; } } /** * @internal */ class _LocoViewBuffer { private $s; public function __construct( $s ){ $this->s = $s; } public function __toString(){ return $this->s; } }