PageRenderTime 72ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

/com/layout.php

http://github.com/unirgy/buckyball
PHP | 2316 lines | 1284 code | 253 blank | 779 comment | 215 complexity | 1910fa1329738f2ea18a2f3fbcd20389 MD5 | raw file
Possible License(s): LGPL-2.1, BSD-3-Clause
  1. <?php
  2. /**
  3. * Copyright 2011 Unirgy LLC
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. *
  17. * @package BuckyBall
  18. * @link http://github.com/unirgy/buckyball
  19. * @author Boris Gurvich <boris@unirgy.com>
  20. * @copyright (c) 2010-2012 Boris Gurvich
  21. * @license http://www.apache.org/licenses/LICENSE-2.0.html
  22. */
  23. /**
  24. * Layout facility to register views and render output from views
  25. */
  26. class BLayout extends BClass
  27. {
  28. /**
  29. * Installed themes registry
  30. *
  31. * @var array
  32. */
  33. protected $_themes = array();
  34. /**
  35. * Default theme name (current area / main module)
  36. *
  37. * @var string|array
  38. */
  39. protected $_defaultTheme;
  40. /**
  41. * Layouts declarations registry
  42. *
  43. * @var array
  44. */
  45. protected $_layouts = array();
  46. /**
  47. * View objects registry
  48. *
  49. * @var array
  50. */
  51. protected $_views = array();
  52. /**
  53. * Main (root) view to be rendered first
  54. *
  55. * @var BView
  56. */
  57. protected $_rootViewName = 'root';
  58. /**
  59. * Main root dir for view files if operating outside of a module
  60. *
  61. * @var mixed
  62. */
  63. protected $_viewRootDir;
  64. /**
  65. * Default class name for newly created views
  66. *
  67. * @var string
  68. */
  69. protected $_defaultViewClass;
  70. /**
  71. * @var array
  72. */
  73. protected static $_metaDirectives = array(
  74. 'remove' => 'BLayout::metaDirectiveRemoveCallback',
  75. 'callback' => 'BLayout::metaDirectiveCallback',
  76. 'layout' => 'BLayout::metaDirectiveIncludeCallback',
  77. 'include' => 'BLayout::metaDirectiveIncludeCallback',
  78. 'root' => 'BLayout::metaDirectiveRootCallback',
  79. 'hook' => 'BLayout::metaDirectiveHookCallback',
  80. 'view' => 'BLayout::metaDirectiveViewCallback',
  81. );
  82. protected static $_renderers = array();
  83. /**
  84. * @var array
  85. */
  86. protected static $_extRenderers = array(
  87. '.php' => array('callback' => null),
  88. );
  89. /**
  90. * @var string
  91. */
  92. protected static $_extRegex = '\.php';
  93. /**
  94. * Shortcut to help with IDE autocompletion
  95. *
  96. * @param bool $new
  97. * @param array $args
  98. * @return BLayout
  99. */
  100. public static function i($new = false, array $args = array())
  101. {
  102. return BClassRegistry::i()->instance(__CLASS__, $args, !$new);
  103. }
  104. /**
  105. * Set root dir for view templates, relative to current module root
  106. *
  107. * @deprecated
  108. * @param string $rootDir
  109. * @return BLayout
  110. */
  111. public function viewRootDir($rootDir = null)
  112. {
  113. if (is_null($rootDir)) {
  114. return $this->getViewRootDir();
  115. }
  116. return $this->setViewRootDir($rootDir);
  117. }
  118. /**
  119. * Get view root dir
  120. * If there is a module in registry as current module, its view root dir will be used, else default one.
  121. *
  122. * @return string
  123. */
  124. public function getViewRootDir()
  125. {
  126. $module = BModuleRegistry::i()->currentModule();
  127. return $module ? $module->view_root_dir : $this->_viewRootDir;
  128. }
  129. /**
  130. * Set view root dir
  131. * If there is current module in registry, set it to it, else set it to layout.
  132. *
  133. * @param $rootDir
  134. * @return $this
  135. */
  136. public function setViewRootDir($rootDir, $module=null)
  137. {
  138. if (is_null($module)) {
  139. $module = BModuleRegistry::i()->currentModule();
  140. }
  141. $isAbsPath = strpos($rootDir, '/') === 0 || strpos($rootDir, ':') === 1;
  142. if ($module) {
  143. $module->view_root_dir = $isAbsPath ? $rootDir : $module->root_dir . '/' . $rootDir;
  144. } else {
  145. $this->_viewRootDir = $rootDir;
  146. }
  147. return $this;
  148. }
  149. /**
  150. * Add extension renderer
  151. *
  152. * Set renderer for particular file extension. E.g. '.php'
  153. * For renderer to work, params should either be array with 'renderer' field
  154. * or a string representing renderer class.
  155. *
  156. * @param string $ext
  157. * @param array $params
  158. * @return $this
  159. */
  160. public function addRenderer($name, $params)
  161. {
  162. if (is_string($name) && is_string($params)) {
  163. $params = array('file_ext' => array($name), 'callback' => $params);
  164. }
  165. if (is_string($params['file_ext'])) {
  166. $params['file_ext'] = explode(';', $params['file_ext']);
  167. }
  168. static::$_renderers[$name] = $params;
  169. foreach ($params['file_ext'] as $ext) {
  170. static::$_extRenderers[$ext] = $params;
  171. }
  172. static::$_extRegex = join('|', array_map('preg_quote', array_keys(static::$_extRenderers)));
  173. BDebug::debug('ADD RENDERER: '.join('; ', $params['file_ext']));
  174. return $this;
  175. }
  176. public function getAllRenderers($asOptions=false)
  177. {
  178. if ($asOptions) {
  179. $options = array();
  180. foreach (static::$_renderers as $k=>$r) {
  181. $options[$k] = !empty($r['description']) ? $r['description'] : $k;
  182. }
  183. asort($options);
  184. return $options;
  185. }
  186. return static::$_renderers;
  187. }
  188. public function getRenderer($name)
  189. {
  190. return !empty(static::$_renderers[$name]) ? static::$_renderers[$name] : null;
  191. }
  192. /**
  193. * Alias for addAllViews()
  194. *
  195. * @deprecated alias
  196. * @param mixed $rootDir
  197. * @param mixed $prefix
  198. * @return BLayout
  199. */
  200. public function allViews($rootDir = null, $prefix = '')
  201. {
  202. return $this->addAllViews($rootDir, $prefix);
  203. }
  204. /**
  205. * Find and register all templates within a folder as view objects
  206. *
  207. * View objects will be named by template file paths, stripped of extension (.php)
  208. *
  209. * @param string $rootDir Folder with view templates, relative to current module root
  210. * Can end with slash or not - make sure to specify
  211. * @param string $prefix Optional: add prefix to view names
  212. * @return BLayout
  213. */
  214. public function addAllViews($rootDir = null, $prefix = '')
  215. {
  216. if (is_null($rootDir)) {
  217. return $this->_views;
  218. }
  219. $curModule = BModuleRegistry::i()->currentModule();
  220. if ($curModule && !BUtil::isPathAbsolute($rootDir)) {
  221. $rootDir = $curModule->root_dir . '/' . $rootDir;
  222. }
  223. if (!is_dir($rootDir)) {
  224. BDebug::warning('Not a valid directory: ' . $rootDir);
  225. return $this;
  226. }
  227. $rootDir = realpath($rootDir);
  228. $this->setViewRootDir($rootDir);
  229. $files = BUtil::globRecursive($rootDir . '/*');
  230. if (!$files) {
  231. return $this;
  232. }
  233. if ($prefix) {
  234. $prefix = rtrim($prefix, '/') . '/';
  235. }
  236. $re = '#^(' . preg_quote(realpath($rootDir) . '/', '#') . ')(.*)(' . static::$_extRegex . ')$#';
  237. foreach ($files as $file) {
  238. if (!is_file($file)) {
  239. continue;
  240. }
  241. if (preg_match($re, $file, $m)) {
  242. //$this->view($prefix.$m[2], array('template'=>$m[2].$m[3]));
  243. $viewParams = array('template' => $file, 'file_ext' => $m[3]);
  244. $viewParams['renderer'] = static::$_extRenderers[$m[3]]['callback'];
  245. $this->addView($prefix . $m[2], $viewParams);
  246. }
  247. }
  248. BEvents::i()->fire(__METHOD__, array('root_dir'=>$rootDir, 'prefix'=>$prefix, 'module'=>$curModule));
  249. return $this;
  250. }
  251. /**
  252. * Set default view class
  253. *
  254. * @todo rename to setDefaultViewClass()
  255. * @param mixed $className
  256. * @return BLayout
  257. */
  258. public function defaultViewClass($className)
  259. {
  260. $this->_defaultViewClass = $className;
  261. return $this;
  262. }
  263. /**
  264. * Register or retrieve a view object
  265. *
  266. * @todo remove adding view from here
  267. * @param string $viewName
  268. * @param array $params View parameters
  269. * - template: optional, for templated views
  270. * - view_class: optional, for custom views
  271. * - module_name: optional, to use template from a specific module
  272. * @param boolean $reset update or reset view params //TODO
  273. * @return BView|BLayout
  274. */
  275. public function view($viewName, $params = null, $reset = false)
  276. {
  277. if ($params) {
  278. $this->addView($viewName, $params, $reset);
  279. return $this;
  280. }
  281. return $this->getView($viewName);
  282. }
  283. /**
  284. * Not sure whether to leave view() for convenience
  285. *
  286. * Return registered view
  287. *
  288. * @param mixed $viewName
  289. * @return null|BView
  290. */
  291. public function getView($viewName)
  292. {
  293. return isset($this->_views[$viewName]) ? $this->_views[$viewName] : BViewEmpty::i();
  294. }
  295. /**
  296. * Add or update view to layout
  297. * Adds or updates a view to layout.
  298. * If view already exists, will replace its params with provided ones.
  299. *
  300. * @param string|array $viewName
  301. * @param string|array $params if string - view class name
  302. * @param bool $reset
  303. * @return $this
  304. * @throws BException
  305. */
  306. public function addView($viewName, $params = array(), $reset = false)
  307. {
  308. if (is_array($viewName)) {
  309. foreach ($viewName as $i => $view) {
  310. if (!is_numeric($i)) {
  311. throw new BException(BLocale::_('Invalid argument: %s', print_r($viewName, 1)));
  312. }
  313. $this->addView($view[0], $view[1], $reset); // if self::view is possible to disappear better not use it.
  314. }
  315. return $this;
  316. }
  317. if (is_string($params)) {
  318. $params = array('view_class' => $params);
  319. }
  320. if (empty($params['module_name']) && ($moduleName = BModuleRegistry::i()->currentModuleName())) {
  321. $params['module_name'] = $moduleName;
  322. }
  323. $viewAlias = !empty($params['view_alias']) ? $params['view_alias'] : $viewName;
  324. if (!isset($this->_views[$viewAlias]) || !empty($params['view_class'])) {
  325. if (empty($params['view_class'])) {
  326. /*
  327. if (!empty($params['module_name'])) {
  328. $viewClass = BApp::m($params['module_name'])->default_view_class;
  329. if ($viewClass) {
  330. $params['view_class'] = $viewClass;
  331. }
  332. } else
  333. */
  334. if (!empty($this->_defaultViewClass)) {
  335. $params['view_class'] = $this->_defaultViewClass;
  336. }
  337. }
  338. $this->_views[$viewAlias] = BView::i()->factory($viewName, $params);
  339. BEvents::i()->fire('BLayout::view:add:' . $viewAlias, array(
  340. 'view' => $this->_views[$viewAlias],
  341. ));
  342. } else {
  343. $this->_views[$viewAlias]->setParam($params);
  344. BEvents::i()->fire('BLayout::view:update:' . $viewAlias, array(
  345. 'view' => $this->_views[$viewAlias],
  346. ));
  347. }
  348. return $this;
  349. }
  350. /**
  351. * Find a view by matching its name to a regular expression
  352. *
  353. * @param string $re
  354. * @return array
  355. */
  356. public function findViewsRegex($re)
  357. {
  358. $views = array();
  359. foreach ($this->_views as $viewName => $view) {
  360. if (preg_match($re, $viewName)) {
  361. $views[$viewName] = $view;
  362. }
  363. }
  364. return $views;
  365. }
  366. /**
  367. * Set or retrieve main (root) view object
  368. *
  369. * @deprecated
  370. * @param string $viewName
  371. * @return BView|BLayout
  372. */
  373. public function rootView($viewName = BNULL)
  374. {
  375. if (BNULL === $viewName) {
  376. // return $this->_rootViewName ? $this->view($this->_rootViewName) : null;
  377. return $this->getRootView(); // the above seems like this method?!
  378. }
  379. /*
  380. if (empty($this->_views[$viewName])) {
  381. throw new BException(BLocale::_('Invalid view name for main view: %s', $viewName));
  382. }
  383. */
  384. $this->_rootViewName = $viewName;
  385. return $this;
  386. }
  387. /**
  388. * Set root view name
  389. * @param string $viewName
  390. * @return $this
  391. */
  392. public function setRootView($viewName)
  393. {
  394. $this->_rootViewName = $viewName;
  395. return $this;
  396. }
  397. /**
  398. * @return BLayout|BView|null
  399. */
  400. public function getRootView()
  401. {
  402. return $this->_rootViewName ? $this->getView($this->_rootViewName) : null;
  403. }
  404. /**
  405. * @return string
  406. */
  407. public function getRootViewName()
  408. {
  409. return $this->_rootViewName;
  410. }
  411. /**
  412. * Clone view object to another name
  413. *
  414. * @param string $from
  415. * @param string $to
  416. * @return BView
  417. */
  418. public function cloneView($from, $to = BNULL)
  419. {
  420. if (BNULL === $to) {
  421. $to = $from . '-copy';
  422. for ($i = 2; !empty($this->_views[$to]); $i++) {
  423. $to = $from . '-copy' . $i;
  424. }
  425. }
  426. $this->_views[$to] = clone $this->_views[$from];
  427. $this->_views[$to]->setParam('view_name', $to);
  428. return $this->_views[$to];
  429. }
  430. /**
  431. * Register a call back to a hook
  432. *
  433. * @param string $hookName
  434. * @param mixed $callback
  435. * @param array $args
  436. * @return $this
  437. */
  438. public function hook($hookName, $callback, $args = array(), $alias = null)
  439. {
  440. BEvents::i()->on('BLayout::hook:' . $hookName, $callback, $args, $alias);
  441. return $this;
  442. }
  443. /**
  444. * Register a view as call back to a hook
  445. * $viewName should either be a string with a name of view,
  446. * or an array in which first field is view name and the rest are view parameters.
  447. *
  448. * @param string $hookName
  449. * @param string|array $viewName
  450. * @param array $args
  451. * @return $this
  452. */
  453. public function hookView($hookName, $viewName, $args = array())
  454. {
  455. if (is_array($viewName)) {
  456. $params = $viewName;
  457. $viewName = array_shift($params);
  458. BLayout::i()->addView($viewName, $params);
  459. }
  460. $view = BLayout::i()->getView($viewName);
  461. if (!$view) {
  462. BDebug::warning('Invalid view name: ' . $viewName, 1);
  463. return $this;
  464. }
  465. //$view->set($args);
  466. return $this->hook($hookName, $view, $args, $viewName);
  467. }
  468. public function hookClear($hookName, $viewNames)
  469. {
  470. $eventHlp = BEvents::i();
  471. $eventName = 'BLayout::hook:' . $hookName;
  472. if (true === $viewNames || 'ALL' === $viewNames) {
  473. $eventHlp->off($eventName, true);
  474. } else {
  475. foreach ((array)$viewNames as $clearViewName) {
  476. $eventHlp->off($eventName, $clearViewName);
  477. }
  478. }
  479. return $this;
  480. }
  481. /**
  482. *
  483. * @deprecated
  484. * @param mixed $layoutName
  485. * @param mixed $layout
  486. * @return BLayout
  487. */
  488. public function layout($layoutName, $layout = null)
  489. {
  490. if (is_array($layoutName) || !is_null($layout)) {
  491. $this->addLayout($layoutName, $layout);
  492. } else {
  493. $this->applyLayout($layoutName);
  494. }
  495. return $this;
  496. }
  497. /**
  498. * Load layout update from file
  499. *
  500. * @param string $layoutFilename
  501. * @return BLayout
  502. */
  503. public function loadLayout($layoutFilename)
  504. {
  505. #echo "<pre>"; debug_print_backtrace(); echo "</pre>";
  506. $ext = strtolower(pathinfo($layoutFilename, PATHINFO_EXTENSION));
  507. if (!BUtil::isPathAbsolute($layoutFilename)) {
  508. $mod = BModuleRegistry::i()->currentModule();
  509. if ($mod) {
  510. $layoutFilename = $mod->root_dir.'/'.$layoutFilename;
  511. }
  512. }
  513. BDebug::debug('LAYOUT.LOAD: '.$layoutFilename);
  514. switch ($ext) {
  515. case 'yml': case 'yaml': $layoutData = BYAML::i()->load($layoutFilename); break;
  516. case 'json': $layoutData = json_decode(file_get_contents($layoutFilename)); break;
  517. case 'php': $layoutData = include($layoutFilename); break;
  518. default: throw new BException('Unknown layout file type: '.$layoutFilename);
  519. }
  520. BLayout::i()->addLayout($layoutData);
  521. return $this;
  522. }
  523. /**
  524. * Load layout update after theme has been initialized
  525. *
  526. * @param string $layoutFilename
  527. * @return BLayout
  528. */
  529. public function loadLayoutAfterTheme($layoutFilename)
  530. {
  531. if (!BUtil::isPathAbsolute($layoutFilename)) {
  532. $mod = BModuleRegistry::i()->currentModule();
  533. if ($mod) {
  534. $layoutFilename = $mod->root_dir.'/'.$layoutFilename;
  535. }
  536. }
  537. $this->onAfterTheme(function() use($layoutFilename) {
  538. BLayout::i()->loadLayout($layoutFilename);
  539. });
  540. return $this;
  541. }
  542. /**
  543. * @param $layoutName
  544. * @param null $layout
  545. * @return $this
  546. */
  547. public function addLayout($layoutName, $layout = null)
  548. {
  549. if (is_array($layoutName)) {
  550. foreach ($layoutName as $l => $def) {
  551. $this->addLayout($l, $def);
  552. }
  553. return $this;
  554. }
  555. if (!is_array($layout)) {
  556. BDebug::debug('LAYOUT.ADD ' . $layoutName . ': Invalid or empty layout');
  557. } else {
  558. if (!isset($this->_layouts[$layoutName])) {
  559. BDebug::debug('LAYOUT.ADD ' . $layoutName);
  560. $this->_layouts[$layoutName] = $layout;
  561. } else {
  562. BDebug::debug('LAYOUT.UPDATE ' . $layoutName);
  563. $this->_layouts[$layoutName] = array_merge_recursive($this->_layouts[$layoutName], $layout);
  564. }
  565. }
  566. return $this;
  567. }
  568. /**
  569. * @param $layoutName
  570. * @return $this
  571. */
  572. public function applyLayout($layoutName)
  573. {
  574. if (empty($this->_layouts[$layoutName])) {
  575. BDebug::debug('LAYOUT.EMPTY ' . $layoutName);
  576. return $this;
  577. }
  578. BDebug::debug('LAYOUT.APPLY ' . $layoutName);
  579. // collect callbacks
  580. $callbacks = array();
  581. foreach ($this->_layouts[$layoutName] as $d) {
  582. if (empty($d['type'])) {
  583. if (!is_array($d)) {
  584. var_dump($layoutName, $d);
  585. }
  586. if (!empty($d[0])) {
  587. $d['type'] = $d[0];
  588. } else {
  589. foreach ($d as $k=>$n) {
  590. if (!empty(self::$_metaDirectives[$k])) {
  591. $d['type'] = $k;
  592. $d['name'] = $n;
  593. break;
  594. }
  595. }
  596. }
  597. if (empty($d['type'])) {
  598. BDebug::dump($d);
  599. }
  600. }
  601. $d['type'] = trim($d['type']);
  602. if (empty($d['type']) || empty(self::$_metaDirectives[$d['type']])) {
  603. BDebug::error('Unknown directive: ' . $d['type']);
  604. continue;
  605. }
  606. if (empty($d['name']) && !empty($d[1])) {
  607. $d['name'] = $d[1];
  608. }
  609. $d['name'] = trim($d['name']);
  610. $d['layout_name'] = $layoutName;
  611. $callback = self::$_metaDirectives[$d['type']];
  612. if ($d['type'] === 'remove') {
  613. if ($d['name'] === 'ALL') { //TODO: allow removing specific instructions
  614. BDebug::debug('LAYOUT.REMOVE');
  615. $callbacks = array();
  616. }
  617. } else {
  618. $callbacks[] = array($callback, $d);
  619. }
  620. }
  621. // perform all callbacks
  622. foreach ($callbacks as $cb) {
  623. call_user_func($cb[0], $cb[1]);
  624. }
  625. return $this;
  626. }
  627. /**
  628. * @param $d
  629. */
  630. public function metaDirectiveCallback($d)
  631. {
  632. call_user_func($d['name'], $d);
  633. }
  634. /**
  635. * @param $d
  636. */
  637. public function metaDirectiveRemoveCallback($d)
  638. {
  639. //TODO: implement
  640. }
  641. /**
  642. * @param $d
  643. */
  644. public function metaDirectiveIncludeCallback($d)
  645. {
  646. if ($d['name'] == $d['layout_name']) { // simple 1 level recursion stop
  647. BDebug::error('Layout recursion detected: ' . $d['name']);
  648. return;
  649. }
  650. static $layoutsApplied = array();
  651. if (!empty($layoutsApplied[$d['name']]) && empty($d['repeat'])) {
  652. return;
  653. }
  654. $layoutsApplied[$d['name']] = 1;
  655. $this->applyLayout($d['name']);
  656. }
  657. /**
  658. * @param array $d
  659. */
  660. public function metaDirectiveRootCallback($d)
  661. {
  662. $this->setRootView($d['name']);
  663. }
  664. /**
  665. * @param array $d
  666. */
  667. public function metaDirectiveHookCallback($d)
  668. {
  669. $args = !empty($d['args']) ? $d['args'] : array();
  670. if (!empty($d['position'])) {
  671. $args['position'] = $d['position'];
  672. }
  673. if (!empty($d['callbacks'])) {
  674. foreach ($d['callbacks'] as $cb) {
  675. $this->hook($d['name'], $cb, $args);
  676. }
  677. }
  678. if (!empty($d['clear'])) {
  679. $this->hookClear($d['name'], $d['clear']);
  680. }
  681. if (!empty($d['views'])) {
  682. foreach ((array)$d['views'] as $v) {
  683. $this->hookView($d['name'], $v, $args);
  684. }
  685. if (!empty($d['use_meta'])) {
  686. $this->view($v)->useMetaData();
  687. }
  688. }
  689. }
  690. /**
  691. * @param $d
  692. */
  693. public function metaDirectiveViewCallback($d)
  694. {
  695. $view = $this->getView($d['name']);
  696. if (!empty($d['set'])) {
  697. foreach ($d['set'] as $k => $v) {
  698. $view->set($k, $v);
  699. }
  700. }
  701. if (!empty($d['param'])) {
  702. foreach ($d['param'] as $k => $v) {
  703. $view->setParam($k, $v);
  704. }
  705. }
  706. if (!empty($d['do'])) {
  707. foreach ($d['do'] as $args) {
  708. $method = array_shift($args);
  709. BDebug::debug('LAYOUT.view.do ' . $method);
  710. call_user_func_array(array($view, $method), $args);
  711. }
  712. }
  713. }
  714. /**
  715. * @deprecated
  716. *
  717. * @param mixed $themeName
  718. * @return BLayout
  719. */
  720. public function defaultTheme($themeName = null)
  721. {
  722. if (is_null($themeName)) {
  723. return $this->_defaultTheme;
  724. }
  725. $this->_defaultTheme = $themeName;
  726. BDebug::debug('THEME.DEFAULT: ' . $themeName);
  727. return $this;
  728. }
  729. /**
  730. * @param $themeName
  731. * @return $this
  732. */
  733. public function setDefaultTheme($themeName)
  734. {
  735. $this->_defaultTheme = $themeName;
  736. BDebug::debug('THEME.DEFAULT: ' . $themeName);
  737. return $this;
  738. }
  739. /**
  740. * @return array|string
  741. */
  742. public function getDefaultTheme()
  743. {
  744. return $this->_defaultTheme;
  745. }
  746. /**
  747. * @param $themeName
  748. * @param $params
  749. * @return $this
  750. */
  751. public function addTheme($themeName, $params)
  752. {
  753. BDebug::debug('THEME.ADD ' . $themeName);
  754. $this->_themes[$themeName] = $params;
  755. return $this;
  756. }
  757. /**
  758. * @param null $area
  759. * @param bool $asOptions
  760. * @return array
  761. */
  762. public function getThemes($area = null, $asOptions = false)
  763. {
  764. if (is_null($area)) {
  765. return $this->_themes;
  766. }
  767. $themes = array();
  768. foreach ($this->_themes as $name => $theme) {
  769. if (!empty($theme['area']) && $theme['area'] === $area) {
  770. if ($asOptions) {
  771. $themes[$name] = !empty($theme['description']) ? $theme['description'] : $name;
  772. } else {
  773. $themes[$name] = $theme;
  774. }
  775. }
  776. }
  777. return $themes;
  778. }
  779. /**
  780. * @param null $themeName
  781. * @return $this
  782. */
  783. public function applyTheme($themeName = null)
  784. {
  785. if (is_null($themeName)) {
  786. if (!$this->_defaultTheme) {
  787. BDebug::error('Empty theme supplied and no default theme is set');
  788. }
  789. $themeName = $this->_defaultTheme;
  790. }
  791. if (is_array($themeName)) {
  792. foreach ($themeName as $n) {
  793. $this->applyTheme($n);
  794. }
  795. return $this;
  796. }
  797. BDebug::debug('THEME.APPLY ' . $themeName);
  798. BEvents::i()->fire('BLayout::applyTheme:before', array('theme_name' => $themeName));
  799. $this->loadTheme($themeName);
  800. BEvents::i()->fire('BLayout::applyTheme:after', array('theme_name' => $themeName));
  801. return $this;
  802. }
  803. public function loadTheme($themeName)
  804. {
  805. if (empty($this->_themes[$themeName])) {
  806. BDebug::warning('Invalid theme name: ' . $themeName);
  807. return false;
  808. }
  809. $theme = $this->_themes[$themeName];
  810. $area = BApp::i()->get('area');
  811. if (!empty($theme['area']) && !in_array($area, (array)$theme['area'])) {
  812. BDebug::debug('Theme ' . $themeName . ' can not be used in ' . $area);
  813. return false;
  814. }
  815. if (!empty($theme['parent'])) {
  816. foreach ((array)$theme['parent'] as $parentThemeName) {
  817. if ($this->loadTheme($parentThemeName)) {
  818. break; // load the first available parent theme
  819. }
  820. }
  821. }
  822. BEvents::i()->fire('BLayout::loadTheme:before', array('theme_name' => $themeName, 'theme' => $theme));
  823. $modRootDir = !empty($theme['module_name']) ? BApp::m($theme['module_name'])->root_dir.'/' : '';
  824. if (!empty($theme['layout'])) {
  825. BLayout::i()->loadLayout($modRootDir.$theme['layout']);
  826. }
  827. if (!empty($theme['views'])) {
  828. BLayout::i()->addAllViews($modRootDir.$theme['views']);
  829. }
  830. if (!empty($theme['callback'])) {
  831. BUtil::i()->call($theme['callback']);
  832. }
  833. BEvents::i()->fire('BLayout::loadTheme:after', array('theme_name' => $themeName, 'theme' => $theme));
  834. return true;
  835. }
  836. /**
  837. * Shortcut for event registration
  838. * @param $callback
  839. * @return $this
  840. */
  841. public function onAfterTheme($callback)
  842. {
  843. BEvents::i()->on('BLayout::applyTheme:after', $callback);
  844. return $this;
  845. }
  846. /**
  847. * Dispatch layout event, for both general observers and route specific observers
  848. *
  849. * Observers should watch for these events:
  850. * - BLayout::{event}
  851. * - BLayout::{event}: GET {route}
  852. *
  853. * @param mixed $eventName
  854. * @param mixed $routeName
  855. * @param mixed $args
  856. * @return array
  857. */
  858. public function dispatch($eventName, $routeName = null, $args = array())
  859. {
  860. if (is_null($routeName) && ($route = BRouting::i()->currentRoute())) {
  861. $args['route_name'] = $routeName = $route->route_name;
  862. }
  863. $result = BEvents::i()->fire("BLayout::{$eventName}", $args);
  864. $routes = is_string($routeName) ? explode(',', $routeName) : (array)$routeName;
  865. foreach ($routes as $route) {
  866. $args['route_name'] = $route;
  867. $r2 = BEvents::i()->fire("BLayout::{$eventName}: {$route}", $args);
  868. $result = BUtil::arrayMerge($result, $r2);
  869. }
  870. return $result;
  871. }
  872. /**
  873. * Render layout starting with main (root) view
  874. *
  875. * @param string $routeName Optional: render a specific route, default current route
  876. * @param array $args Render arguments
  877. * @return mixed
  878. */
  879. public function render($routeName = null, $args = array())
  880. {
  881. $this->dispatch('render:before', $routeName, $args);
  882. $rootView = $this->getRootView();
  883. BDebug::debug('LAYOUT.RENDER ' . var_export($rootView, 1));
  884. if (!$rootView) {
  885. BDebug::error(BLocale::_('Main view not found: %s', $this->_rootViewName));
  886. }
  887. $result = $rootView->render($args);
  888. $args['output'] =& $result;
  889. $this->dispatch('render:after', $routeName, $args);
  890. //BSession::i()->dirty(false); // disallow session change during layout render
  891. return $result;
  892. }
  893. /**
  894. * @return void
  895. */
  896. public function debugPrintViews()
  897. {
  898. foreach ($this->_views as $viewName => $view) {
  899. echo $viewName . ':<pre>';
  900. print_r($view);
  901. echo '</pre><hr>';
  902. }
  903. }
  904. /**
  905. *
  906. */
  907. public function debugPrintLayouts()
  908. {
  909. echo "<pre>";
  910. print_r($this->_layouts);
  911. echo "</pre>";
  912. }
  913. }
  914. /**
  915. * First parent view class
  916. */
  917. class BView extends BClass
  918. {
  919. /**
  920. * @var
  921. */
  922. protected static $_renderer;
  923. /**
  924. * @var string
  925. */
  926. protected static $_metaDataRegex = '#<!--\s*\{\s*([^:]+):\s*(.*?)\s*\}\s*-->#';
  927. /**
  928. * View parameters
  929. * - view_class
  930. * - template
  931. * - module_name
  932. * - args
  933. *
  934. * @var array
  935. */
  936. protected $_params;
  937. /**
  938. * Factory to generate view instances
  939. *
  940. * @param string $viewName
  941. * @param array $params
  942. * @return BView
  943. */
  944. static public function factory($viewName, array $params = array())
  945. {
  946. $params['view_name'] = $viewName;
  947. $className = !empty($params['view_class']) ? $params['view_class'] : get_called_class();
  948. $view = BClassRegistry::i()->instance($className, $params);
  949. return $view;
  950. }
  951. /**
  952. * Constructor, set initial view parameters
  953. *
  954. * @param array $params
  955. * @return BView
  956. */
  957. public function __construct(array $params)
  958. {
  959. $this->_params = $params;
  960. }
  961. /**
  962. * Retrieve view parameters
  963. *
  964. * @param string $key
  965. * @return mixed|BView
  966. */
  967. public function param($key = null)
  968. {
  969. if (is_null($key)) {
  970. return $this->_params;
  971. }
  972. return isset($this->_params[$key]) ? $this->_params[$key] : null;
  973. }
  974. /**
  975. * @param $key
  976. * @param null $value
  977. * @return $this
  978. */
  979. public function setParam($key, $value = null)
  980. {
  981. if (is_array($key)) {
  982. foreach ($key as $k => $v) {
  983. $this->setParam($k, $v);
  984. }
  985. return $this;
  986. }
  987. $this->_params[$key] = $value;
  988. return $this;
  989. }
  990. /**
  991. * @param $key
  992. * @return null
  993. */
  994. public function getParam($key)
  995. {
  996. return isset($this->_params[$key]) ? $this->_params[$key] : null;
  997. }
  998. /**
  999. * @param $name
  1000. * @param null $value
  1001. * @return $this
  1002. */
  1003. public function set($name, $value = null)
  1004. {
  1005. if (is_array($name)) {
  1006. foreach ($name as $k => $v) {
  1007. $this->_params['args'][$k] = $v;
  1008. }
  1009. return $this;
  1010. }
  1011. $this->_params['args'][$name] = $value;
  1012. return $this;
  1013. }
  1014. /**
  1015. * @param $name
  1016. * @return null
  1017. */
  1018. public function get($name)
  1019. {
  1020. return isset($this->_params['args'][$name]) ? $this->_params['args'][$name] : null;
  1021. }
  1022. /**
  1023. * @return array
  1024. */
  1025. public function getAllArgs()
  1026. {
  1027. return !empty($this->_params['args']) ? $this->_params['args'] : array();
  1028. }
  1029. /**
  1030. * Magic method to retrieve argument, accessible from view/template as $this->var
  1031. *
  1032. * @param string $name
  1033. * @return mixed
  1034. */
  1035. public function __get($name)
  1036. {
  1037. return $this->get($name);
  1038. }
  1039. /**
  1040. * Magic method to set argument, stored in params['args']
  1041. *
  1042. * @param string $name
  1043. * @param mixed $value
  1044. * @return $this
  1045. */
  1046. public function __set($name, $value)
  1047. {
  1048. return $this->set($name, $value);
  1049. }
  1050. /**
  1051. * Magic method to check if argument is set
  1052. *
  1053. * @param string $name
  1054. * @return bool
  1055. */
  1056. public function __isset($name)
  1057. {
  1058. return isset($this->_params['args'][$name]);
  1059. }
  1060. /**
  1061. * Magic method to unset argument
  1062. *
  1063. * @param string $name
  1064. */
  1065. public function __unset($name)
  1066. {
  1067. unset($this->_params['args'][$name]);
  1068. }
  1069. /**
  1070. * Retrieve view object
  1071. *
  1072. * @todo detect multi-level circular references
  1073. * @param string $viewName
  1074. * @param array $params
  1075. * @throws BException
  1076. * @return BView|null
  1077. */
  1078. public function view($viewName, $params = null)
  1079. {
  1080. if ($viewName === $this->param('view_name')) {
  1081. throw new BException(BLocale::_('Circular reference detected: %s', $viewName));
  1082. }
  1083. $view = BLayout::i()->getView($viewName);
  1084. if ($view && $params) {
  1085. $view->set($params);
  1086. }
  1087. return $view;
  1088. }
  1089. /**
  1090. * Collect output from subscribers of a layout event
  1091. *
  1092. * @param string $hookName
  1093. * @param array $args
  1094. * @return string
  1095. */
  1096. public function hook($hookName, $args = array())
  1097. {
  1098. $args['_viewname'] = $this->param('view_name');
  1099. $result = '';
  1100. $debug = BDebug::is('DEBUG');
  1101. if ($debug) {
  1102. $result .= "<!-- START HOOK: {$hookName} -->\n";
  1103. }
  1104. $result .= join('', BEvents::i()->fire('BView::hook:before', array('view' => $this, 'name' => $hookName)));
  1105. $result .= join('', BEvents::i()->fire('BLayout::hook:' . $hookName, $args));
  1106. $result .= join('', BEvents::i()->fire('BView::hook:after', array('view' => $this, 'name' => $hookName)));
  1107. if ($debug) {
  1108. $result .= "<!-- END HOOK: {$hookName} -->\n";
  1109. }
  1110. return $result;
  1111. }
  1112. /**
  1113. * @param string $defaultFileExt
  1114. * @param bool $quiet
  1115. * @return BView|mixed|string
  1116. */
  1117. public function getTemplateFileName($fileExt = null, $quiet = false)
  1118. {
  1119. if (is_null($fileExt)) {
  1120. $fileExt = $this->getParam('file_ext');
  1121. }
  1122. $template = $this->param('template');
  1123. if (!$template && ($viewName = $this->param('view_name'))) {
  1124. $template = $viewName . $fileExt;
  1125. }
  1126. if ($template) {
  1127. if (!BUtil::isPathAbsolute($template)) {
  1128. $template = BLayout::i()->getViewRootDir() . '/' . $template;
  1129. }
  1130. if (!is_readable($template) && !$quiet) {
  1131. BDebug::notice('TEMPLATE NOT FOUND: ' . $template);
  1132. } else {
  1133. BDebug::debug('TEMPLATE ' . $template);
  1134. }
  1135. }
  1136. return $template;
  1137. }
  1138. /**
  1139. * Used by external renderers to include compiled PHP file within $this context
  1140. *
  1141. * @param mixed $file
  1142. */
  1143. public function renderFile($file)
  1144. {
  1145. ob_start();
  1146. include $file;
  1147. return ob_get_clean();
  1148. }
  1149. public function renderEval($source)
  1150. {
  1151. ob_start();
  1152. eval($source);
  1153. return ob_get_clean();
  1154. }
  1155. /**
  1156. * View class specific rendering
  1157. *
  1158. * @return string
  1159. */
  1160. protected function _render()
  1161. {
  1162. $renderer = $this->param('renderer');
  1163. if ($renderer) {
  1164. return call_user_func($renderer, $this);
  1165. }
  1166. ob_start();
  1167. include $this->getTemplateFileName();
  1168. return ob_get_clean();
  1169. }
  1170. /**
  1171. * General render public method
  1172. *
  1173. * @param array $args
  1174. * @param bool $retrieveMetaData
  1175. * @return string
  1176. */
  1177. public function render(array $args = array(), $retrieveMetaData = true)
  1178. {
  1179. $debug = BDebug::is('DEBUG') && !$this->get('no_debug');
  1180. $viewName = $this->param('view_name');
  1181. $timer = BDebug::debug('RENDER.VIEW ' . $viewName);
  1182. if ($this->param('raw_text') !== null) {
  1183. return $this->param('raw_text');
  1184. }
  1185. foreach ($args as $k => $v) {
  1186. $this->_params['args'][$k] = $v;
  1187. }
  1188. if (($modName = $this->param('module_name'))) {
  1189. BModuleRegistry::i()->pushModule($modName);
  1190. }
  1191. $result = '';
  1192. if (!$this->_beforeRender()) {
  1193. BDebug::debug('BEFORE.RENDER failed');
  1194. if ($debug) {
  1195. $result .= "<!-- FAILED VIEW: {$viewName} -->\n";
  1196. }
  1197. return $result;
  1198. }
  1199. $showDebugTags = $debug && $modName && $viewName && BLayout::i()->getRootViewName()!==$viewName;
  1200. if ($showDebugTags) {
  1201. $result .= "<!-- START VIEW: @{$modName}/{$viewName} -->\n";
  1202. }
  1203. $result .= join('', BEvents::i()->fire('BView::render:before', array('view' => $this)));
  1204. $viewContent = $this->_render();
  1205. if ($retrieveMetaData) {
  1206. $metaData = array();
  1207. if (preg_match_all(static::$_metaDataRegex, $viewContent, $matches, PREG_SET_ORDER)) {
  1208. foreach ($matches as $m) {
  1209. $metaData[$m[1]] = $m[2];
  1210. $viewContent = str_replace($m[0], '', $viewContent);
  1211. }
  1212. }
  1213. $this->setParam('meta_data', $metaData);
  1214. }
  1215. $result .= $viewContent;
  1216. $result .= join('', BEvents::i()->fire('BView::render:after', array('view' => $this)));
  1217. if ($showDebugTags) {
  1218. $result .= "<!-- END VIEW: @{$modName}/{$viewName} -->\n";
  1219. }
  1220. BDebug::profile($timer);
  1221. $this->_afterRender();
  1222. if ($modName) {
  1223. BModuleRegistry::i()->popModule();
  1224. }
  1225. return $result;
  1226. }
  1227. /**
  1228. * Use meta data declared in the view template to set head meta tags
  1229. */
  1230. public function useMetaData()
  1231. {
  1232. $this->render();
  1233. $metaData = $this->param('meta_data');
  1234. if ($metaData) {
  1235. if (!empty($metaData['layout.yml'])) {
  1236. $layoutData = BYAML::i()->parse(trim($metaData['layout.yml']));
  1237. BLayout::i()->addLayout('viewproxy-metadata', $layoutData)->applyLayout('viewproxy-metadata');
  1238. }
  1239. if (($head = $this->view('head'))) {
  1240. foreach ($metaData as $k=>$v) {
  1241. $k = strtolower($k);
  1242. switch ($k) {
  1243. case 'title':
  1244. $head->addTitle($v); break;
  1245. case 'meta_title': case 'meta_description': case 'meta_keywords':
  1246. $head->meta(str_replace('meta_','',$k), $v); break;
  1247. }
  1248. }
  1249. }
  1250. }
  1251. return $this;
  1252. }
  1253. /**
  1254. * @return bool
  1255. */
  1256. protected function _beforeRender()
  1257. {
  1258. return true;
  1259. }
  1260. /**
  1261. *
  1262. */
  1263. protected function _afterRender()
  1264. {
  1265. }
  1266. /**
  1267. * Clear parameters to avoid circular reference memory leaks
  1268. *
  1269. */
  1270. public function clear()
  1271. {
  1272. unset($this->_params);
  1273. }
  1274. /**
  1275. * Clear params on destruct
  1276. *
  1277. */
  1278. public function __destruct()
  1279. {
  1280. $this->clear();
  1281. }
  1282. /**
  1283. * Render as string
  1284. *
  1285. * If there's exception during render, output as string as well
  1286. *
  1287. * @return string
  1288. */
  1289. public function __toString()
  1290. {
  1291. try {
  1292. $result = $this->render();
  1293. } catch (PDOException $e) {
  1294. $result = '<hr>' . get_class($e) . ': ' . $e->getMessage() . '<hr>' . ORM::get_last_query() . '<hr>';
  1295. } catch (Exception $e) {
  1296. $result = '<hr>' . get_class($e) . ': ' . $e->getMessage() . '<hr>';
  1297. }
  1298. return $result;
  1299. }
  1300. /**
  1301. * Escape HTML
  1302. *
  1303. * @param string $str
  1304. * @param array $args
  1305. * @return string
  1306. */
  1307. public function q($str, $args = array())
  1308. {
  1309. if (is_null($str)) {
  1310. return '';
  1311. }
  1312. if (!is_scalar($str)) {
  1313. var_dump($str);
  1314. return ' ** ERROR ** ';
  1315. }
  1316. return htmlspecialchars($args ? BUtil::sprintfn($str, $args) : $str);
  1317. }
  1318. /**
  1319. * @param $str
  1320. * @param null $tags
  1321. * @return string
  1322. */
  1323. public function s($str, $tags = null)
  1324. {
  1325. return strip_tags($str, $tags);
  1326. }
  1327. /**
  1328. * @deprecated by BUtil::optionsHtml()
  1329. * @param $options
  1330. * @param string $default
  1331. * @return string
  1332. */
  1333. public function optionsHtml($options, $default = '')
  1334. {
  1335. return BUtil::optionsHtml($options, $default);
  1336. }
  1337. /**
  1338. * Send email using the content of the view as body using standard PHP mail()
  1339. *
  1340. * Templates can include the following syntax for default headers:
  1341. * - <!--{ From: Support <support@example.com> }-->
  1342. * - <!--{ Subject: New order notification #<?php echo $this->order_id?> }-->
  1343. *
  1344. * $p accepts following parameters:
  1345. * - to: email OR "name" <email>
  1346. * - from: email OR "name" <email>
  1347. * - subject: email subject
  1348. * - cc: email OR "name" <email> OR array of these
  1349. * - bcc: same as cc
  1350. * - reply-to
  1351. * - return-path
  1352. *
  1353. * All parameters are also available in the template as $this->{param}
  1354. *
  1355. * @param array|string $p if string, used as "To:" header
  1356. * @return bool true if successful
  1357. */
  1358. public function email($p = array())
  1359. {
  1360. if (is_string($p)) {
  1361. $p = array('to' => $p);
  1362. }
  1363. $body = $this->render($p, true);
  1364. $data = array_merge(
  1365. array_change_key_case($this->param('meta_data'), CASE_LOWER),
  1366. array_change_key_case($p, CASE_LOWER)
  1367. );
  1368. $data['body'] = $body;
  1369. return BEmail::i()->send($data);
  1370. }
  1371. /**
  1372. * Translate string within view class method or template
  1373. *
  1374. * @param string $string
  1375. * @param array $params
  1376. * @param string $module if null, try to get current view module
  1377. * @return \false|string
  1378. */
  1379. public function _($string, $params = array(), $module = null)
  1380. {
  1381. if (empty($module) && !empty($this->_params['module_name'])) {
  1382. $module = $this->_params['module_name'];
  1383. }
  1384. return BLocale::_($string, $params, $module);
  1385. }
  1386. protected $_validators = array();
  1387. public function validator($formName, $data = null)
  1388. {
  1389. if (empty($this->_validators[$formName])) {
  1390. $this->_validators[$formName] = BValidateViewHelper::i(true, array(
  1391. 'form' => $formName,
  1392. 'data' => $data,
  1393. ));
  1394. }
  1395. return $this->_validators[$formName];
  1396. }
  1397. }
  1398. /**
  1399. * Helper view to avoid errors of using views from disabled modules
  1400. */
  1401. class BViewEmpty extends BView
  1402. {
  1403. public function render(array $args = array(), $retrieveMetaData = true)
  1404. {
  1405. return '';
  1406. }
  1407. }
  1408. /**
  1409. * View dedicated for rendering HTML HEAD tags
  1410. */
  1411. class BViewHead extends BView
  1412. {
  1413. /**
  1414. * @var array
  1415. */
  1416. protected $_title = array();
  1417. /**
  1418. * @var string
  1419. */
  1420. protected $_titleSeparator = ' :: ';
  1421. /**
  1422. * @var bool
  1423. */
  1424. protected $_titleReverse = true;
  1425. /**
  1426. * Substitution variables
  1427. *
  1428. * @var array
  1429. */
  1430. protected $_subst = array();
  1431. /**
  1432. * Meta tags
  1433. *
  1434. * @var array
  1435. */
  1436. protected $_meta = array();
  1437. /**
  1438. * External resources (JS and CSS)
  1439. *
  1440. * @var array
  1441. */
  1442. protected $_elements = array();
  1443. /**
  1444. * Support for head.js
  1445. *
  1446. * @see http://headjs.com/
  1447. * @var array
  1448. */
  1449. protected $_headJs = array('enabled' => false, 'loaded' => false, 'jquery' => null, 'scripts' => array());
  1450. /**
  1451. * Support for require.js
  1452. *
  1453. * @see http://requirejs.org/
  1454. * @var array
  1455. */
  1456. protected $_requireJs = array('config' => array(), 'run' => array());
  1457. /**
  1458. * Default tag templates for JS and CSS resources
  1459. *
  1460. * @var array
  1461. */
  1462. protected $_defaultTag = array(
  1463. 'js' => '<script type="text/javascript" src="%s" %a></script>',
  1464. 'js_raw' => '<script type="text/javascript" %a>%c</script>',
  1465. 'css' => '<link rel="stylesheet" type="text/css" href="%s" %a/>',
  1466. 'css_raw' => '<style type="text/css" %a>%c</style>',
  1467. //'less' => '<link rel="stylesheet" type="text/less" href="%s" %a/>',
  1468. 'less' => '<link rel="stylesheet/less" type="text/css" href="%s" %a/>',
  1469. 'icon' => '<link rel="icon" href="%s" type="image/x-icon" %a/><link rel="shortcut icon" href="%s" type="image/x-icon" %a/>',
  1470. );
  1471. /**
  1472. * Current IE <!--[if]--> context
  1473. *
  1474. * @var string
  1475. */
  1476. protected $_currentIfContext = null;
  1477. /**
  1478. * @param $from
  1479. * @param null $to
  1480. * @return $this|string
  1481. */
  1482. public function subst($from, $to = null)
  1483. {
  1484. if (is_null($to)) {
  1485. return str_replace(array_keys($this->_subst), array_values($this->_subst), $from);
  1486. }
  1487. $this->_subst['{' . $from . '}'] = $to;
  1488. return $this;
  1489. }
  1490. /**
  1491. * Enable/disable head js
  1492. *
  1493. * @param bool $enable
  1494. * @return $this
  1495. */
  1496. public function headJs($enable = true)
  1497. {
  1498. $this->_headJs['enabled'] = $enable;
  1499. return $this;
  1500. }
  1501. /**
  1502. * Alias for addTitle($title)
  1503. *
  1504. * @deprecated
  1505. * @param mixed $title
  1506. * @param bool $start
  1507. * @return BViewHead
  1508. */
  1509. public function title($title, $start = false)
  1510. {
  1511. $this->addTitle($title, $start);
  1512. }
  1513. /**
  1514. * Add meta tag, or return meta tag(s)
  1515. *
  1516. * @deprecated
  1517. *
  1518. * @param string $name If not specified, will return all meta tags as string
  1519. * @param string $content If not specified, will return meta tag by name
  1520. * @param bool $httpEquiv Whether the tag is http-equiv
  1521. * @return BViewHead
  1522. */
  1523. public function meta($name = null, $content = null, $httpEquiv = false)
  1524. {
  1525. if (is_null($content)) {
  1526. return $this->getMeta($name);
  1527. }
  1528. $this->addMeta($name, $content, $httpEquiv);
  1529. return $this;
  1530. }
  1531. public function csrf_token()
  1532. {
  1533. $this->addMeta('csrf-token', BSession::i()->csrfToken());
  1534. return $this;
  1535. }
  1536. /**
  1537. * Add canonical link
  1538. * @param $href
  1539. * @return $this
  1540. */
  1541. public function canonical($href)
  1542. {
  1543. $this->addElement('link', 'canonical', array('tag' => '<link rel="canonical" href="' . $href . '"/>'));
  1544. return $this;
  1545. }
  1546. /**
  1547. * Add rss link
  1548. *
  1549. * @param $href
  1550. */
  1551. public function rss($href)
  1552. {
  1553. $this->addElement('link', 'rss', array('tag' => '<link rel="alternate" type="application/rss+xml" title="RSS" href="' . $href . '">'));
  1554. }
  1555. /**
  1556. * Enable direct call of different item types as methods (js, css, icon, less)
  1557. *
  1558. * @param string $name
  1559. * @param array $args
  1560. * @return mixed
  1561. */
  1562. public function __call($name, $args)
  1563. {
  1564. if (!empty($this->_defaultTag[$name])) {
  1565. array_unshift($args, $name);
  1566. return call_user_func_array(array($this, 'addElement'), $args);
  1567. } else {
  1568. BDebug::error('Invalid method: ' . $name);
  1569. }
  1570. }
  1571. public function removeAll()
  1572. {
  1573. $this->_elements = array();
  1574. $this->_headJs = array();
  1575. return $this;
  1576. }
  1577. /**
  1578. * Remove JS/CSS elements by type and pattern (strpos)
  1579. *
  1580. * @param string $type
  1581. * @param string $pattern
  1582. * @return BViewHead
  1583. */
  1584. public function remove($type, $pattern)
  1585. {
  1586. if ($type === 'js' && $this->_headJs['loaded']) {
  1587. foreach ($this->_headJs['scripts'] as $i => $file) {
  1588. if (true===$pattern || strpos($file, $pattern) !== false) {
  1589. unset($this->_headJs['scripts'][$i]);
  1590. }
  1591. }
  1592. }
  1593. foreach ($this->_elements as $k => $args) {
  1594. if (strpos($k, $type) === 0 && (true===$pattern || strpos($k, $pattern) !== false)) {
  1595. unset($this->_elements[$k]);
  1596. }
  1597. }
  1598. return $this;
  1599. }
  1600. /**
  1601. * Set title
  1602. * This will replace any current title
  1603. *
  1604. * @param $title
  1605. * @return $this
  1606. */
  1607. public function setTitle($title)
  1608. {
  1609. $this->_title = array($title);
  1610. return $this;
  1611. }
  1612. /**
  1613. * Add title
  1614. * Add title to be appended to or replace current titles
  1615. *
  1616. * @param $title
  1617. * @param bool $start
  1618. * @return $this
  1619. */
  1620. public function addTitle($title, $start = false)
  1621. {
  1622. if ($start) {
  1623. array_splice($this->_title, 0, 1, $title);
  1624. } else {
  1625. $this->_title[] = $title;
  1626. }
  1627. return $this;
  1628. }
  1629. /**
  1630. * Set title separator
  1631. * Set character or string to be used to separate title values.
  1632. *
  1633. * @param $sep
  1634. * @return $this
  1635. */
  1636. public function setTitleSeparator($sep)
  1637. {
  1638. $this->_titleSeparator = $sep;
  1639. return $this;
  1640. }
  1641. /**
  1642. * Should title be composed in reverse order
  1643. *
  1644. * @param $reverse
  1645. * @return $this
  1646. */
  1647. public function setTitleReverse($reverse)
  1648. {
  1649. $this->_titleReverse = $reverse;
  1650. return $this;
  1651. }
  1652. /**
  1653. * Compose and return title
  1654. * Title is composed by all elements in $_title object field separated by _titleSeparator
  1655. *
  1656. * @return string
  1657. */
  1658. public function getTitle()
  1659. {
  1660. if (!$this->_title) {
  1661. return '';
  1662. }
  1663. if ($this->_titleReverse) {
  1664. $this->_title = array_reverse($this->_title);
  1665. }
  1666. return '<title>' . $this->q(join($this->_titleSeparator, $this->_title)) . '</title>';
  1667. }
  1668. /**
  1669. * Get meta tags
  1670. * If name is null, returns all meta tags joined
  1671. * else returns named meta tag or null if name is not in _meta array
  1672. *
  1673. * @param null $name
  1674. * @return null|string
  1675. */
  1676. public function getMeta($name = null)
  1677. {
  1678. if (is_null($name)) {
  1679. return join("\n", $this->_meta);
  1680. }
  1681. return !empty($this->_meta[$name]) ? $this->_meta[$name] : null;
  1682. }
  1683. /**
  1684. * Add meta tag
  1685. *
  1686. * @param $name
  1687. * @param $content
  1688. * @param bool $httpEquiv
  1689. * @return $this
  1690. */
  1691. public function addMeta($name, $content, $httpEquiv = false)
  1692. {
  1693. if ($httpEquiv) {
  1694. $this->_meta[$name] = '<meta http-equiv="' . $name . '" content="' . htmlspecialchars($content) . '" />';
  1695. } else {
  1696. $this->_meta[$name] = '<meta name="' . $name . '" content="' . htmlspecialchars($content) . '" />';
  1697. }
  1698. return $this;
  1699. }
  1700. /**
  1701. * Add element
  1702. * @param $type
  1703. * @param $name
  1704. * @param array $args
  1705. * @return $this
  1706. */
  1707. public function addElement($type, $name, $args = array())
  1708. {
  1709. //echo "<pre>"; debug_print_backtrace(); echo "</pre>";
  1710. //var_dump($type, $name, $args);
  1711. if (is_string($args)) {
  1712. $args = array('content' => $args);
  1713. }
  1714. if (!empty($args['alias'])) {
  1715. $args['file'] = trim($name);
  1716. $name = trim($args['alias']);
  1717. }
  1718. if (!isset($args['module_name']) && ($moduleName = BModuleRegistry::i()->currentModuleName())) {
  1719. $args['module_name'] = $moduleName;
  1720. }
  1721. if (!isset($args['if']) && $this->_currentIfContext) {
  1722. $args['if'] = $this->_currentIfContext;
  1723. }
  1724. $args['type'] = $type;
  1725. if (empty($args['position'])) {
  1726. $this->_elements[$type . ':' . $name] = (array)$args;
  1727. } else {
  1728. $this->_elements = BUtil::arrayInsert(
  1729. $this->_elements,
  1730. array($type . ':' . $name => (array)$args),
  1731. $args['position']
  1732. );
  1733. #echo "<pre>"; print_r($this->_elements); echo "</pre>";
  1734. }
  1735. if ($this->_headJs['enabled']) {
  1736. $basename = basename($name);
  1737. if ($basename === 'head.js' || $basename === 'head.min.js' || $basename === 'head.load.min.js') {
  1738. $this->_headJs['loaded'] = $name;
  1739. }
  1740. }
  1741. #BDebug::debug('EXT.RESOURCE '.$name.': '.print_r($this->_elements[$type.':'.$name], 1));
  1742. return $this;
  1743. }
  1744. public function src($file, $ts = false)
  1745. {
  1746. if (is_array($file)) {
  1747. $files = array();
  1748. foreach ($file as $k=>$f) {
  1749. $files[$k] = $this->src($f, $ts);
  1750. }
  1751. return $files;
  1752. }
  1753. if ($file[0] === '@') { // @Mod_Name/file.ext
  1754. preg_match('#^@([^/]+)(.*)$#', $file, $m);
  1755. $mod = BApp::m($m[1]);
  1756. if (!$mod) {
  1757. BDebug::notice('Module not found: ' . $file);
  1758. return '';
  1759. }
  1760. $fsFile = BApp::m($m[1])->root_dir . $m[2];
  1761. $file = BApp::m($m[1])->baseSrc() . $m[2];
  1762. if ($ts && file_exists($fsFile)) {
  1763. $file .= '?' . substr(md5(filemtime($fsFile)), 0, 10);
  1764. }
  1765. } elseif (preg_match('#\{([A-Za-z0-9_]+)\}#', $file, $m)) { // {Mod_Name}/file.ext (deprecated)
  1766. $mod = BApp::m($m[1]);
  1767. if (!$mod) {
  1768. BDebug::notice('Module not found: ' . $file);
  1769. return '';
  1770. }
  1771. $fsFile = str_replace('{' . $m[1] . '}', BApp::m($m[1])->root_dir, $file);
  1772. $file = str_replace('{' . $m[1] . '}', BApp::m($m[1])->baseSrc(), $file);
  1773. if ($ts && file_exists($fsFile)) {
  1774. $file .= '?' . substr(md5(filemtime($fsFile)), 0, 10);
  1775. }
  1776. }
  1777. return $file;
  1778. }
  1779. /**
  1780. * @param $type
  1781. * @param $name
  1782. * @return mixed|null|string
  1783. */
  1784. public function getElement($type, $name)
  1785. {
  1786. $typeName = $type . ':' . $name;
  1787. if (!isset($this->_elements[$typeName])) {
  1788. return null;
  1789. }
  1790. $args = $this->_elements[$typeName];
  1791. $file = !empty($args['file']) ? $args['file'] : $name;
  1792. $file = $this->src($file, true);
  1793. if (strpos($file, 'http:') === false && strpos($file, 'https:') === false && $file[0] !== '/') {
  1794. $module = !empty($args['module_name']) ? BModuleRegistry::i()->module($args['module_name']) : null;
  1795. $baseUrl = $module ? $module->baseSrc() : BApp::baseUrl();
  1796. $file = $baseUrl . '/' . $file;
  1797. }
  1798. if ($type === 'js' && $this->_headJs['loaded'] && $this->_headJs['loaded'] !== $name
  1799. && empty($args['separate']) && empty($args['tag']) && empty($args['params']) && empty($args['if'])
  1800. ) {
  1801. if (!$this->_headJs['jquery'] && strpos($name, 'jquery') !== false) {
  1802. $this->_headJs['jquery'] = $file;
  1803. } else {
  1804. $this->_headJs['scripts'][] = $file;
  1805. }
  1806. return '';
  1807. }
  1808. $tag = !empty($args['tag']) ? $args['tag'] : $this->_defaultTag[$type];
  1809. $tag = str_replace('%s', htmlspecialchars($file), $tag);
  1810. $tag = str_replace('%c', !empty($args['content']) ? $args['content'] : '', $tag);
  1811. $tag = str_replace('%a', !empty($args['params']) ? $args['params'] : '', $tag);
  1812. if (!empty($args['if'])) {
  1813. $tag = '<!--[if ' . $args['if'] . ']>' . $tag . '<![endif]-->';
  1814. }
  1815. return $tag;
  1816. }
  1817. /**
  1818. * @return mixed
  1819. */
  1820. public function getAllElements()
  1821. {
  1822. $result = array();
  1823. $res1 = array();
  1824. foreach ($this->_elements as $typeName => $els) {
  1825. list($type, $name) = explode(':', $typeName, 2);
  1826. //$result[] = $this->getElement($type, $name);
  1827. $res1[$type == 'css' ? 0 : 1][] = $this->getElement($type, $name);
  1828. }
  1829. for ($i = 0; $i <= 1; $i++) {
  1830. if (!empty($res1[$i])) $result[] = join("\n", $res1[$i]);
  1831. }
  1832. return preg_replace('#\n{2,}#', "\n", join("\n", $result));
  1833. }
  1834. /**
  1835. * Start/Stop IE if context
  1836. *
  1837. * @param mixed $context
  1838. * @return $this
  1839. */
  1840. public function ifContext($context = null)
  1841. {
  1842. $this->_currentIfContext = $context;
  1843. return $this;
  1844. }
  1845. public function requireModulePath($name = null, $path = null)
  1846. {
  1847. if (is_null($name)) {
  1848. $m = BApp::m();
  1849. $name = $m->name;
  1850. } else {
  1851. $m = BApp::m($name);
  1852. }
  1853. if (is_null($path)) {
  1854. $path = trim($m->base_src, '/').'/js';
  1855. }
  1856. BDebug::debug(__METHOD__.':'.$name.':'.$path);
  1857. $this->_requireJs['config']['paths'][$name] = $path;
  1858. return $this;
  1859. }
  1860. public function requireJs($name, $path, $shim = null)
  1861. {
  1862. $this->_requireJs['config']['paths'][$name] = $path;
  1863. if (!is_null($shim)) {
  1864. $this->_requireJs['config']['shim'][$name] = $shim;
  1865. }
  1866. return $this;
  1867. }
  1868. public function requireConfig($config)
  1869. {
  1870. $this->_requireJs['config'] = BUtil::arrayMerge($this->_requireJs['config'], $config);
  1871. return $this;
  1872. }
  1873. public function requireRun($names)
  1874. {
  1875. $this->_requireJs['run'] = array_merge($this->_requireJs['run'], (array)$names);
  1876. return $this;
  1877. }
  1878. public function renderRequireJs()
  1879. {
  1880. $jsArr = array();
  1881. if (!empty($this->_requireJs['config'])) {
  1882. $config = $this->_requireJs['config'];
  1883. if (empty($config['baseUrl'])) {
  1884. $config['baseUrl'] = BConfig::i()->get('web/base_src');
  1885. }
  1886. if (!empty($config['paths'])) {
  1887. foreach ($config['paths'] as $name => $file) {
  1888. $config['paths'][$name] = $this->src($file);
  1889. }
  1890. }
  1891. // if (BDebug::is('DEBUG')) {
  1892. // $config['urlArgs'] = 'bust='.time();
  1893. // }
  1894. $jsArr[] = "require.config(".BUtil::toJavaScript($config)."); ";
  1895. }
  1896. if (!empty($this->_requireJs['run'])) {
  1897. $jsArr[] = "require(['" . join("', '", $this->_requireJs['run']) . "']);";
  1898. }
  1899. return join("\n", $jsArr);
  1900. }
  1901. /**
  1902. * Render the view
  1903. *
  1904. * If param['template'] is not specified, return meta+css+js tags
  1905. *
  1906. * @param array $args
  1907. * @param bool $retrieveMetaData
  1908. * @return string
  1909. */
  1910. public function render(array $args = array(), $retrieveMetaData = true)
  1911. {
  1912. if (!$this->param('template')) {
  1913. $html = $this->getTitle() . "\n" . $this->getMeta() . "\n" . $this->getAllElements();
  1914. $scriptsArr = array();
  1915. if ($this->_headJs['scripts'] || $this->_headJs['jquery']) {
  1916. if ($this->_headJs['scripts']) {
  1917. $scriptsArr[] = 'head.js("' . join('", "', $this->_headJs['scripts']) . '");';
  1918. }
  1919. if ($this->_headJs['jquery']) {
  1920. $scriptsArr[] = 'head.js({jquery:"' . $this->_headJs['jquery'] . '"}, function() { jQuery.fn.ready = head; ' . $scripts . '});';
  1921. }
  1922. }
  1923. $requireJs = $this->renderRequireJs();
  1924. if ($requireJs) {
  1925. $scriptsArr[] = $requireJs;
  1926. }
  1927. if ($scriptsArr) {
  1928. $html .= "<script>" . join("\n", $scriptsArr) . "</script>";
  1929. }
  1930. return $html;
  1931. }
  1932. return parent::render($args);
  1933. }
  1934. }
  1935. /**
  1936. * View subclass to store and render lists of views
  1937. *
  1938. * @deprecated by BLayout::i()->hook()
  1939. */
  1940. class BViewList extends BView
  1941. {
  1942. /**
  1943. * Child blocks
  1944. *
  1945. * @var array
  1946. */
  1947. protected $_children = array();
  1948. /**
  1949. * Last registered position to sort children
  1950. *
  1951. * @var int
  1952. */
  1953. protected $_lastPosition = 0;
  1954. /**
  1955. * Append block to the list
  1956. *
  1957. * @param string|array $viewname array or comma separated list of view names
  1958. * @param array $params
  1959. * @return BViewList
  1960. */
  1961. public function append($viewname, array $params = array())
  1962. {
  1963. if (is_string($viewname)) {
  1964. $viewname = explode(',', $viewname);
  1965. }
  1966. if (isset($params['position'])) {
  1967. $this->_lastPosition = $params['position'];
  1968. }
  1969. foreach ($viewname as $v) {
  1970. $params['name'] = $v;
  1971. $params['position'] = $this->_lastPosition++;
  1972. $this->_children[] = $params;
  1973. }
  1974. return $this;
  1975. }
  1976. /**
  1977. * Append plain text to the list
  1978. *
  1979. * A new view object will be created for each text entry with random name
  1980. *
  1981. * @param string $text
  1982. * @return BViewList
  1983. */
  1984. public function appendText($text)
  1985. {
  1986. $layout = BLayout::i();
  1987. for ($viewname = md5(mt_rand()); $layout->getView($viewname);) ;
  1988. $layout->addView($viewname, array('raw_text' => (string)$text));
  1989. $this->append($viewname);
  1990. return $this;
  1991. }
  1992. /**
  1993. * Find child view by its content
  1994. *
  1995. * May be slow, use sparringly
  1996. *
  1997. * @param string $content
  1998. * @return BView|null
  1999. */
  2000. public function find($content)
  2001. {
  2002. foreach ($this->_children as $i => $child) {
  2003. $view = $this->view($child['name']);
  2004. if (strpos($view->render(), $content) !== false) {
  2005. return $view;
  2006. }
  2007. }
  2008. return null;
  2009. }
  2010. /**
  2011. * Remove child view from the list
  2012. *
  2013. * @param string $viewname
  2014. * @return BViewList
  2015. */
  2016. public function remove($viewname)
  2017. {
  2018. if (true === $viewname) {
  2019. $this->_children = array();
  2020. return $this;
  2021. }
  2022. foreach ($this->_children as $i => $child) {
  2023. if ($child['name'] == $viewname) {
  2024. unset($this->_children[$i]);
  2025. break;
  2026. }
  2027. }
  2028. return $this;
  2029. }
  2030. /**
  2031. * Render the children views
  2032. *
  2033. * @param array $args
  2034. * @param bool $retrieveMetaData
  2035. * @throws BException
  2036. * @return string
  2037. */
  2038. public function render(array $args = array(), $retrieveMetaData = true)
  2039. {
  2040. $output = array();
  2041. uasort($this->_children, array($this, 'sortChildren'));
  2042. $layout = BLayout::i();
  2043. foreach ($this->_children as $child) {
  2044. $childView = $layout->getView($child['name']);
  2045. if (!$childView) {
  2046. throw new BException(BLocale::_('Invalid view name: %s', $child['name']));
  2047. }
  2048. $output[] = $childView->render($args);
  2049. }
  2050. return join('', $output);
  2051. }
  2052. /**
  2053. * Sort child views by their position
  2054. *
  2055. * @param mixed $a
  2056. * @param mixed $b
  2057. * @return int
  2058. */
  2059. public function sortChildren($a, $b)
  2060. {
  2061. return $a['position'] < $b['position'] ? -1 : ($a['position'] > $b['position'] ? 1 : 0);
  2062. }
  2063. }