View.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. <?php
  2. /**
  3. * Acts as an object wrapper for HTML pages with embedded PHP, called "views".
  4. * Variables can be assigned with the view object and referenced locally within
  5. * the view.
  6. *
  7. * @package KO7
  8. * @category Base
  9. *
  10. * @copyright (c) 2007-2020 Kohana Team
  11. * @copyright (c) since 2016 Koseven Team
  12. * @license https://koseven.dev/LICENSE
  13. */
  14. abstract class KO7_View {
  15. /**
  16. * @var array Global variables
  17. */
  18. protected static $_global_data = [];
  19. /**
  20. * Returns a new View object. If you do not define the "file" parameter,
  21. * you must call [View::set_filename].
  22. *
  23. * @param string|null $file view filename
  24. * @param iterable $data array of values
  25. * @return View
  26. */
  27. public static function factory($file = NULL, iterable $data = [])
  28. {
  29. return new View($file, $data);
  30. }
  31. /**
  32. * Captures the output that is generated when a view is included.
  33. * The view data will be extracted to make local variables. This method
  34. * is static to prevent object scope resolution.
  35. *
  36. * @param string $ko7_view_filename filename
  37. * @param array $ko7_view_data variables
  38. * @return string
  39. * @throws Throwable
  40. */
  41. protected static function capture($ko7_view_filename, $ko7_view_data)
  42. {
  43. // Import the view variables to local namespace
  44. extract($ko7_view_data, EXTR_SKIP);
  45. if (View::$_global_data)
  46. {
  47. // Import the global view variables to local namespace
  48. extract(View::$_global_data, EXTR_SKIP | EXTR_REFS);
  49. }
  50. // Capture the view output
  51. ob_start();
  52. try
  53. {
  54. // Load the view within the current scope
  55. include $ko7_view_filename;
  56. }
  57. catch (Throwable $e)
  58. {
  59. // Delete the output buffer
  60. ob_end_clean();
  61. // Re-throw the exception
  62. throw new View_Exception(
  63. 'Rendering error in template :view: :error',
  64. [':view' => Debug::path($ko7_view_filename), ':error' => $e],
  65. 0,
  66. $e
  67. );
  68. }
  69. // Get the captured output and close the buffer
  70. return ob_get_clean();
  71. }
  72. /**
  73. * Sets a global variable, similar to [View::set], except that the
  74. * variable will be accessible to all views.
  75. *
  76. * View::set_global($name, $value);
  77. *
  78. * You can also use an array or Traversable object to set several values at once:
  79. *
  80. * // Create the values $food and $beverage in the view
  81. * View::set_global(['food' => 'bread', 'beverage' => 'water']);
  82. *
  83. * [!!] Note: When setting with using Traversable object we're not attaching the whole object to the view,
  84. * i.e. the object's standard properties will not be available in the view context.
  85. *
  86. * @param string|iterable $key variable name or an array of variables
  87. * @param mixed $value value
  88. * @return void
  89. */
  90. public static function set_global($key, $value = NULL)
  91. {
  92. if (is_iterable($key))
  93. {
  94. foreach ($key as $name => $value)
  95. {
  96. View::$_global_data[$name] = $value;
  97. }
  98. }
  99. else
  100. {
  101. View::$_global_data[$key] = $value;
  102. }
  103. }
  104. /**
  105. * Assigns a global variable by reference, similar to [View::bind], except
  106. * that the variable will be accessible to all views.
  107. *
  108. * @param string $key variable name
  109. * @param mixed $value referenced variable
  110. * @return void
  111. */
  112. public static function bind_global($key, &$value)
  113. {
  114. View::$_global_data[$key] =& $value;
  115. }
  116. /**
  117. * @var string Absolute path to view
  118. */
  119. protected $_file;
  120. /**
  121. * @var string Relative path to view
  122. */
  123. protected $_source_file;
  124. /**
  125. * @var array Local variables
  126. */
  127. protected $_data = [];
  128. /**
  129. * Sets the initial view filename and local data. Views should almost
  130. * always only be created using [View::factory].
  131. *
  132. * @param string|null $file view filename
  133. * @param iterable $data array of values
  134. */
  135. public function __construct($file = NULL, iterable $data = [])
  136. {
  137. if ($file !== NULL)
  138. {
  139. $this->set_filename($file);
  140. }
  141. if ($data)
  142. {
  143. $this->set($data);
  144. }
  145. }
  146. /**
  147. * Magic method, searches for the given variable and returns its value.
  148. * Local variables will be returned before global variables.
  149. *
  150. * [!!] If the variable has not yet been set, an exception will be thrown.
  151. *
  152. * @param string $key Variable name
  153. * @return mixed
  154. * @throws View_Exception
  155. */
  156. public function &__get($key)
  157. {
  158. if (array_key_exists($key, $this->_data))
  159. {
  160. return $this->_data[$key];
  161. }
  162. elseif (array_key_exists($key, View::$_global_data))
  163. {
  164. return View::$_global_data[$key];
  165. }
  166. throw new View_Exception(
  167. 'Variable is not set: :var',
  168. [':var' => $key]
  169. );
  170. }
  171. /**
  172. * Magic method, calls [View::set] with the same parameters.
  173. *
  174. * @param string $key variable name
  175. * @param mixed $value value
  176. * @return void
  177. */
  178. public function __set($key, $value)
  179. {
  180. $this->set($key, $value);
  181. }
  182. /**
  183. * Magic method, determines if a variable is set.
  184. *
  185. * [!!] `NULL` variables are not considered to be set by [isset](https://php.net/isset).
  186. *
  187. * @param string $key variable name
  188. * @return bool
  189. */
  190. public function __isset($key)
  191. {
  192. return isset($this->_data[$key]) OR isset(View::$_global_data[$key]);
  193. }
  194. /**
  195. * Magic method, unsets a given variable.
  196. *
  197. * @param string $key variable name
  198. * @return void
  199. */
  200. public function __unset($key)
  201. {
  202. unset($this->_data[$key], View::$_global_data[$key]);
  203. }
  204. /**
  205. * Magic method, returns the output of [View::render].
  206. *
  207. * @return string
  208. */
  209. public function __toString()
  210. {
  211. try
  212. {
  213. return $this->render();
  214. }
  215. catch (Throwable $e)
  216. {
  217. /**
  218. * Display the exception message and halt script execution.
  219. * We use this method here because it's impossible to throw an
  220. * exception from `__toString()`.
  221. */
  222. View_Exception::handler($e);
  223. // This line will never be reached
  224. return '';
  225. }
  226. }
  227. /**
  228. * Sets the view filename.
  229. *
  230. * @param string $file view filename
  231. * @return $this
  232. * @throws View_Exception
  233. */
  234. public function set_filename($file)
  235. {
  236. $path = KO7::find_file('views', $file);
  237. if (! $path)
  238. {
  239. throw new View_Exception(
  240. 'The requested view :file could not be found',
  241. [':file' => $file]
  242. );
  243. }
  244. // Store the file path locally
  245. $this->_file = $path;
  246. $this->_source_file = $file;
  247. return $this;
  248. }
  249. /**
  250. * Gets the view filename.
  251. *
  252. * @param bool $source If true, return source filename, else absolute path
  253. * @return string|null
  254. */
  255. public function get_filename(bool $source = false): ?string
  256. {
  257. return $source ? $this->_source_file : $this->_file;
  258. }
  259. /**
  260. * Assigns a variable by name. Assigned values will be available as a
  261. * variable within the view file.
  262. *
  263. * You can also use an array or Traversable object to set several values at once:
  264. *
  265. * // Create the values $food and $beverage in the view
  266. * $view->set(['food' => 'bread', 'beverage' => 'water']);
  267. *
  268. * [!!] Note: When setting with using Traversable object we're not attaching the whole object to the view,
  269. * i.e. the object's standard properties will not be available in the view context.
  270. *
  271. * @param string|iterable $key variable name or an array of variables
  272. * @param mixed $value value
  273. * @return $this
  274. */
  275. public function set($key, $value = NULL)
  276. {
  277. if (is_iterable($key))
  278. {
  279. foreach ($key as $name => $value)
  280. {
  281. $this->_data[$name] = $value;
  282. }
  283. }
  284. else
  285. {
  286. $this->_data[$key] = $value;
  287. }
  288. return $this;
  289. }
  290. /**
  291. * Assigns a value by reference. The benefit of binding is that values can
  292. * be altered without re-setting them. It is also possible to bind variables
  293. * before they have values. Assigned values will be available as a
  294. * variable within the view file.
  295. *
  296. * @param string $key variable name
  297. * @param mixed $value referenced variable
  298. * @return $this
  299. */
  300. public function bind($key, &$value)
  301. {
  302. $this->_data[$key] =& $value;
  303. return $this;
  304. }
  305. /**
  306. * Renders the view object to a string. Global and local data are merged
  307. * and extracted to create local variables within the view file.
  308. *
  309. * [!!] Global variables with the same key name as local variables will be
  310. * overwritten by the local variable.
  311. *
  312. * @param string|null $file view filename
  313. * @return string
  314. * @throws View_Exception
  315. */
  316. public function render($file = NULL)
  317. {
  318. if ($file !== NULL)
  319. {
  320. $this->set_filename($file);
  321. }
  322. if (! $this->get_filename())
  323. {
  324. throw new View_Exception(
  325. 'You must set the file to use within your view before rendering'
  326. );
  327. }
  328. // Combine local and global data and capture the output
  329. return View::capture($this->get_filename(), $this->_data);
  330. }
  331. }