theme.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788
  1. <?php
  2. /**
  3. * Part of the Fuel framework.
  4. *
  5. * @package Fuel
  6. * @version 1.5
  7. * @author Fuel Development Team
  8. * @license MIT License
  9. * @copyright 2010 - 2013 Fuel Development Team
  10. * @link http://fuelphp.com
  11. */
  12. namespace Fuel\Core;
  13. class ThemeException extends \FuelException {}
  14. /**
  15. * Handles loading theme views and assets.
  16. */
  17. class Theme
  18. {
  19. /**
  20. * All the Theme instances
  21. *
  22. * @var array
  23. */
  24. protected static $instances = array();
  25. /**
  26. * Acts as a Multiton. Will return the requested instance, or will create
  27. * a new named one if it does not exist.
  28. *
  29. * @param string $name The instance name
  30. *
  31. * @return Theme
  32. */
  33. public static function instance($name = '_default_', array $config = array())
  34. {
  35. if ( ! \array_key_exists($name, static::$instances))
  36. {
  37. static::$instances[$name] = static::forge($config);
  38. }
  39. return static::$instances[$name];
  40. }
  41. /**
  42. * Gets a new instance of the Theme class.
  43. *
  44. * @param array $config Optional config override
  45. * @return Theme
  46. */
  47. public static function forge(array $config = array())
  48. {
  49. return new static($config);
  50. }
  51. /**
  52. * @var Asset_Instance $asset Asset instance for this theme instance
  53. */
  54. public $asset = null;
  55. /**
  56. * @var array $paths Possible locations for themes
  57. */
  58. protected $paths = array();
  59. /**
  60. * @var View $template View instance for this theme instance template
  61. */
  62. public $template = null;
  63. /**
  64. * @var array $active Currently active theme
  65. */
  66. protected $active = array(
  67. 'name' => null,
  68. 'path' => null,
  69. 'asset_base' => false,
  70. 'asset_path' => false,
  71. 'info' => array(),
  72. );
  73. /**
  74. * @var array $fallback Fallback theme
  75. */
  76. protected $fallback = array(
  77. 'name' => null,
  78. 'path' => null,
  79. 'asset_base' => false,
  80. 'asset_path' => false,
  81. 'info' => array(),
  82. );
  83. /**
  84. * @var array $config Theme config
  85. */
  86. protected $config = array(
  87. 'active' => 'default',
  88. 'fallback' => 'default',
  89. 'paths' => array(),
  90. 'assets_folder' => 'themes',
  91. 'view_ext' => '.html',
  92. 'require_info_file' => false,
  93. 'info_file_name' => 'theme.info.php',
  94. 'use_modules' => false,
  95. );
  96. /**
  97. * @var array $partials Storage for defined template partials
  98. */
  99. protected $partials = array();
  100. /**
  101. * @var array $chrome Storage for defined partial chrome
  102. */
  103. protected $chrome = array();
  104. /**
  105. * Sets up the theme object. If a config is given, it will not use the config
  106. * file.
  107. *
  108. * @param array $config Optional config override
  109. * @return void
  110. */
  111. public function __construct(array $config = array())
  112. {
  113. if (empty($config))
  114. {
  115. \Config::load('theme', true, false, true);
  116. $config = \Config::get('theme', false);
  117. }
  118. // Order of this addition is important, do not change this.
  119. $this->config = $config + $this->config;
  120. // define the default theme paths...
  121. $this->add_paths($this->config['paths']);
  122. // create a unique asset instance for this theme instance...
  123. $this->asset = \Asset::forge('theme_'.spl_object_hash($this), array('paths' => array()));
  124. // and set the active and the fallback theme
  125. $this->active($this->config['active']);
  126. $this->fallback($this->config['fallback']);
  127. }
  128. /**
  129. * Magic method, returns the output of [static::render].
  130. *
  131. * @return string
  132. * @uses Theme::render
  133. */
  134. public function __toString()
  135. {
  136. try
  137. {
  138. return (string) $this->render();
  139. }
  140. catch (\Exception $e)
  141. {
  142. \Error::exception_handler($e);
  143. return '';
  144. }
  145. }
  146. /**
  147. * Sets the currently active theme. Will return the currently active
  148. * theme. It will throw a \ThemeException if it cannot locate the theme.
  149. *
  150. * @param string $theme Theme name to set active
  151. * @return array The theme array
  152. * @throws \ThemeException
  153. */
  154. public function active($theme = null)
  155. {
  156. return $this->set_theme($theme, 'active');
  157. }
  158. /**
  159. * Sets the fallback theme. This theme will be used if a view or asset
  160. * cannot be found in the active theme. Will return the fallback
  161. * theme. It will throw a \ThemeException if it cannot locate the theme.
  162. *
  163. * @param string $theme Theme name to set active
  164. * @return array The theme array
  165. * @throws \ThemeException
  166. */
  167. public function fallback($theme = null)
  168. {
  169. return $this->set_theme($theme, 'fallback');
  170. }
  171. /**
  172. * Loads a view from the currently active theme, the fallback theme, or
  173. * via the standard FuelPHP cascading file system for views
  174. *
  175. * @param string $view View name
  176. * @param array $data View data
  177. * @param bool $auto_filter Auto filter the view data
  178. * @return View New View object
  179. */
  180. public function view($view, $data = array(), $auto_filter = null)
  181. {
  182. if ($this->active['path'] === null)
  183. {
  184. throw new \ThemeException('You must set an active theme.');
  185. }
  186. return \View::forge($this->find_file($view), $data, $auto_filter);
  187. }
  188. /**
  189. * Loads an asset from the currently loaded theme.
  190. *
  191. * @param string $path Relative path to the asset
  192. * @return string Full asset URL or path if outside docroot
  193. */
  194. public function asset_path($path)
  195. {
  196. if ($this->active['path'] === null)
  197. {
  198. throw new \ThemeException('You must set an active theme.');
  199. }
  200. if ($this->active['asset_base'])
  201. {
  202. return $this->active['asset_base'].$path;
  203. }
  204. else
  205. {
  206. return $this->active['path'].$path;
  207. }
  208. }
  209. /**
  210. * Sets a template for a theme
  211. *
  212. * @param string $template Name of the template view
  213. * @return View
  214. */
  215. public function set_template($template)
  216. {
  217. // make sure the template is a View
  218. if (is_string($template))
  219. {
  220. $this->template = $this->view($template);
  221. }
  222. else
  223. {
  224. $this->template = $template;
  225. }
  226. // return the template view for chaining
  227. return $this->template;
  228. }
  229. /**
  230. * Get the template view so it can be manipulated
  231. *
  232. * @return string|View
  233. * @throws \ThemeException
  234. */
  235. public function get_template()
  236. {
  237. // make sure the partial entry exists
  238. if (empty($this->template))
  239. {
  240. throw new \ThemeException('No valid template could be found. Use set_template() to define a page template.');
  241. }
  242. // return the template
  243. return $this->template;
  244. }
  245. /**
  246. * Render the partials and the theme template
  247. *
  248. * @return string|View
  249. * @throws \ThemeException
  250. */
  251. public function render()
  252. {
  253. // make sure the template to be rendered is defined
  254. if (empty($this->template))
  255. {
  256. throw new \ThemeException('No valid template could be found. Use set_template() to define a page template.');
  257. }
  258. // pre-process all defined partials
  259. foreach ($this->partials as $key => $partials)
  260. {
  261. $output = '';
  262. foreach ($partials as $index => $partial)
  263. {
  264. // render the partial
  265. $output .= $partial->render();
  266. }
  267. // store the rendered output
  268. if ( ! empty($output) and array_key_exists($key, $this->chrome))
  269. {
  270. // encapsulate the partial in the chrome template
  271. $this->partials[$key] = $this->chrome[$key]['view']->set($this->chrome[$key]['var'], $output, false);
  272. }
  273. else
  274. {
  275. // store the partial output
  276. $this->partials[$key] = $output;
  277. }
  278. }
  279. // assign the partials to the template
  280. $this->template->set('partials', $this->partials, false);
  281. // return the template
  282. return $this->template;
  283. }
  284. /**
  285. * Sets a partial for the current template
  286. *
  287. * @param string $section Name of the partial section in the template
  288. * @param string|View|ViewModel $view View, or name of the view
  289. * @param bool $overwrite If true overwrite any already defined partials for this section
  290. * @return View
  291. */
  292. public function set_partial($section, $view, $overwrite = false)
  293. {
  294. // make sure the partial entry exists
  295. array_key_exists($section, $this->partials) or $this->partials[$section] = array();
  296. // make sure the partial is a view
  297. if (is_string($view))
  298. {
  299. $name = $view;
  300. $view = $this->view($view);
  301. }
  302. else
  303. {
  304. $name = 'partial_'.count($this->partials[$section]);
  305. }
  306. // store the partial
  307. if ($overwrite)
  308. {
  309. $this->partials[$section] = array($name => $view);
  310. }
  311. else
  312. {
  313. $this->partials[$section][$name] = $view;
  314. }
  315. // return the partial view object for chaining
  316. return $this->partials[$section][$name];
  317. }
  318. /**
  319. * Get a partial so it can be manipulated
  320. *
  321. * @param string $section Name of the partial section in the template
  322. * @param string $view name of the view
  323. * @return View
  324. * @throws \ThemeException
  325. */
  326. public function get_partial($section, $view)
  327. {
  328. // make sure the partial entry exists
  329. if ( ! array_key_exists($section, $this->partials) or ! array_key_exists($view, $this->partials[$section]))
  330. {
  331. throw new \ThemeException(sprintf('No partial named "%s" can be found in the "%s" section.', $view, $section));
  332. }
  333. return $this->partials[$section][$view];
  334. }
  335. /**
  336. * Sets a chrome for a partial
  337. *
  338. * @param string $section Name of the partial section in the template
  339. * @param string|View|ViewModel $view chrome View, or name of the view
  340. * @param string $var Name of the variable in the chome that will output the partial
  341. *
  342. * @return void
  343. */
  344. public function set_chrome($section, $view, $var = 'content')
  345. {
  346. // make sure the chrome is a view
  347. if (is_string($view))
  348. {
  349. $view = $this->view($view);
  350. }
  351. $this->chrome[$section] = array('var' => $var, 'view' => $view);
  352. }
  353. /**
  354. * Get a set chrome view
  355. *
  356. * @param string $section Name of the partial section in the template
  357. * @param string|View|ViewModel $view chrome View, or name of the view
  358. * @param string $var Name of the variable in the chome that will output the partial
  359. *
  360. * @return void
  361. */
  362. public function get_chrome($section)
  363. {
  364. // make sure the partial entry exists
  365. if ( ! array_key_exists($section, $this->chrome))
  366. {
  367. throw new \ThemeException(sprintf('No chrome for a partial named "%s" can be found.', $section));
  368. }
  369. return $this->chrome[$section]['view'];
  370. }
  371. /**
  372. * Adds the given path to the theme search path.
  373. *
  374. * @param string $path Path to add
  375. * @return void
  376. */
  377. public function add_path($path)
  378. {
  379. $this->paths[] = rtrim($path, DS).DS;
  380. }
  381. /**
  382. * Adds the given paths to the theme search path.
  383. *
  384. * @param array $paths Paths to add
  385. * @return void
  386. */
  387. public function add_paths(array $paths)
  388. {
  389. array_walk($paths, array($this, 'add_path'));
  390. }
  391. /**
  392. * Finds the given theme by searching through all of the theme paths. If
  393. * found it will return the path, else it will return `false`.
  394. *
  395. * @param string $theme Theme to find
  396. * @return string|false Path or false if not found
  397. */
  398. public function find($theme)
  399. {
  400. foreach ($this->paths as $path)
  401. {
  402. if (is_dir($path.$theme))
  403. {
  404. return $path.$theme.DS;
  405. }
  406. }
  407. return false;
  408. }
  409. /**
  410. * Gets an array of all themes in all theme paths, sorted alphabetically.
  411. *
  412. * @return array
  413. */
  414. public function all()
  415. {
  416. $themes = array();
  417. foreach ($this->paths as $path)
  418. {
  419. foreach(glob($path.'*', GLOB_ONLYDIR) as $theme)
  420. {
  421. $themes[] = basename($theme);
  422. }
  423. }
  424. sort($themes);
  425. return $themes;
  426. }
  427. /**
  428. * Get a value from the info array
  429. *
  430. * @return mixed
  431. */
  432. public function get_info($var = null, $default = null, $theme = null)
  433. {
  434. // if no theme is given
  435. if ($theme === null)
  436. {
  437. // if no var to search is given return the entire active info array
  438. if ($var === null)
  439. {
  440. return $this->active['info'];
  441. }
  442. // find the value in the active theme info
  443. if (($value = \Arr::get($this->active['info'], $var, null)) !== null)
  444. {
  445. return $value;
  446. }
  447. // and if not found, check the fallback
  448. elseif (($value = \Arr::get($this->fallback['info'], $var, null)) !== null)
  449. {
  450. return $value;
  451. }
  452. }
  453. // or if we have a specific theme
  454. else
  455. {
  456. // fetch the info from that theme
  457. $info = $this->load_info($theme);
  458. // and return the requested value
  459. return $var === null ? $info : \Arr::get($info, $var, $default);
  460. }
  461. // not found, return the given default value
  462. return $default;
  463. }
  464. /**
  465. * Set a value in the info array
  466. *
  467. * @return Theme
  468. */
  469. public function set_info($var, $value = null, $type = 'active')
  470. {
  471. if ($type == 'active')
  472. {
  473. \Arr::set($this->active['info'], $var, $value);
  474. }
  475. elseif ($type == 'fallback')
  476. {
  477. \Arr::set($this->fallback['info'], $var, $value);
  478. }
  479. // return for chaining
  480. return $this;
  481. }
  482. /**
  483. * Load in the theme.info file for the given (or active) theme.
  484. *
  485. * @param string $theme Name of the theme (null for active)
  486. * @return array Theme info array
  487. */
  488. public function load_info($theme = null)
  489. {
  490. if ($theme === null)
  491. {
  492. $theme = $this->active;
  493. }
  494. if (is_array($theme))
  495. {
  496. $path = $theme['path'];
  497. $name = $theme['name'];
  498. }
  499. else
  500. {
  501. $path = $this->find($theme);
  502. $name = $theme;
  503. $theme = array(
  504. 'name' => $name,
  505. 'path' => $path
  506. );
  507. }
  508. if ( ! $path)
  509. {
  510. throw new \ThemeException(sprintf('Could not find theme "%s".', $theme));
  511. }
  512. if (($file = $this->find_file($this->config['info_file_name'], array($theme))) == $this->config['info_file_name'])
  513. {
  514. if ($this->config['require_info_file'])
  515. {
  516. throw new \ThemeException(sprintf('Theme "%s" is missing "%s".', $name, $this->config['info_file_name']));
  517. }
  518. else
  519. {
  520. return array();
  521. }
  522. }
  523. return \Config::load($file, false, true);
  524. }
  525. /**
  526. * Save the theme.info file for the active (or fallback) theme.
  527. *
  528. * @param string $type Name of the theme (null for active)
  529. * @return array Theme info array
  530. */
  531. public function save_info($type = 'active')
  532. {
  533. if ($type == 'active')
  534. {
  535. $theme = $this->active;
  536. }
  537. elseif ($type == 'fallback')
  538. {
  539. $theme = $this->fallback;
  540. }
  541. else
  542. {
  543. throw new \ThemeException('No location found to save the info file to.');
  544. }
  545. if ( ! $theme['path'])
  546. {
  547. throw new \ThemeException(sprintf('Could not find theme "%s".', $theme['name']));
  548. }
  549. if ( ! ($file = $this->find_file($this->config['info_file_name'], array($theme['name']))))
  550. {
  551. throw new \ThemeException(sprintf('Theme "%s" is missing "%s".', $theme['name'], $this->config['info_file_name']));
  552. }
  553. return \Config::save($file, $theme['info']);
  554. }
  555. /**
  556. * Enable or disable the use of modules. If enabled, every theme view loaded
  557. * will be prefixed with the module name, so you don't have to hardcode the
  558. * module name as a view file prefix
  559. *
  560. * @param $enable enable if true, disable if false
  561. * @return Theme
  562. */
  563. public function use_modules($enable = true)
  564. {
  565. $this->config['use_modules'] = (bool) $enable;
  566. // return for chaining
  567. return $this;
  568. }
  569. /**
  570. * Find the absolute path to a file in a set of Themes. You can optionally
  571. * send an array of themes to search. If you do not, it will search active
  572. * then fallback (in that order).
  573. *
  574. * @param string $view name of the view to find
  575. * @param array $themes optional array of themes to search
  576. * @return string absolute path to the view
  577. * @throws \ThemeException when not found
  578. */
  579. protected function find_file($view, $themes = null)
  580. {
  581. if ($themes === null)
  582. {
  583. $themes = array($this->active, $this->fallback);
  584. }
  585. // determine the path prefix
  586. $path_prefix = '';
  587. if ($this->config['use_modules'] and $module = \Request::active()->module)
  588. {
  589. $path_prefix = $module.DS;
  590. }
  591. foreach ($themes as $theme)
  592. {
  593. $ext = pathinfo($view, PATHINFO_EXTENSION) ?
  594. '.'.pathinfo($view, PATHINFO_EXTENSION) : $this->config['view_ext'];
  595. $file = (pathinfo($view, PATHINFO_DIRNAME) ?
  596. str_replace(array('/', DS), DS, pathinfo($view, PATHINFO_DIRNAME)).DS : '').
  597. pathinfo($view, PATHINFO_FILENAME);
  598. if (empty($theme['find_file']))
  599. {
  600. if (is_file($path = $theme['path'].$path_prefix.$file.$ext))
  601. {
  602. return $path;
  603. }
  604. elseif (is_file($path = $theme['path'].$file.$ext))
  605. {
  606. return $path;
  607. }
  608. }
  609. else
  610. {
  611. if ($path = \Finder::search($theme['path'].$path_prefix, $file, $ext))
  612. {
  613. return $path;
  614. }
  615. }
  616. }
  617. // not found, return the viewname to fall back to the standard View processing
  618. return $view;
  619. }
  620. /**
  621. * Sets a theme.
  622. *
  623. * @param string $theme Theme name to set active
  624. * @param string $type name of the internal theme array to set
  625. * @return array The theme array
  626. * @throws \ThemeException
  627. */
  628. protected function set_theme($theme = null, $type = 'active')
  629. {
  630. // remove the defined theme asset paths from the asset instance
  631. empty($this->active['asset_path']) or $this->asset->remove_path($this->active['asset_path']);
  632. empty($this->fallback['asset_path']) or $this->asset->remove_path($this->fallback['asset_path']);
  633. // set the fallback theme
  634. if ($theme !== null)
  635. {
  636. $this->{$type} = $this->create_theme_array($theme);
  637. }
  638. // add the asset paths to the asset instance
  639. empty($this->fallback['asset_path']) or $this->asset->add_path($this->fallback['asset_path']);
  640. empty($this->active['asset_path']) or $this->asset->add_path($this->active['asset_path']);
  641. return $this->{$type};
  642. }
  643. /**
  644. * Creates a theme array by locating the given theme and setting all of the
  645. * option. It will throw a \ThemeException if it cannot locate the theme.
  646. *
  647. * @param string $theme Theme name to set active
  648. * @return array The theme array
  649. * @throws \ThemeException
  650. */
  651. protected function create_theme_array($theme)
  652. {
  653. if ( ! is_array($theme))
  654. {
  655. if ( ! $path = $this->find($theme))
  656. {
  657. throw new \ThemeException(sprintf('Theme "%s" could not be found.', $theme));
  658. }
  659. $theme = array(
  660. 'name' => $theme,
  661. 'path' => $path,
  662. );
  663. }
  664. else
  665. {
  666. if ( ! isset($theme['name']) or ! isset($theme['path']))
  667. {
  668. throw new \ThemeException('Theme name and path must be given in array config.');
  669. }
  670. }
  671. // load the theme info file
  672. if ( ! isset($theme['info']))
  673. {
  674. $theme['info'] = $this->load_info($theme);
  675. }
  676. if ( ! isset($theme['asset_base']))
  677. {
  678. // determine the asset location and base URL
  679. $assets_folder = rtrim($this->config['assets_folder'], DS).'/';
  680. // all theme files are inside the docroot
  681. if (strpos($path, DOCROOT) === 0 and is_dir($path.$assets_folder))
  682. {
  683. $theme['asset_path'] = $path.$assets_folder;
  684. $theme['asset_base'] = str_replace(DOCROOT, '', $theme['asset_path']);
  685. }
  686. // theme views and templates are outside the docroot
  687. else
  688. {
  689. $theme['asset_base'] = $assets_folder.$theme['name'].'/';
  690. }
  691. }
  692. if ( ! isset($theme['asset_path']) and strpos($theme['asset_base'], '://') === false)
  693. {
  694. $theme['asset_path'] = DOCROOT.$theme['asset_base'];
  695. }
  696. // always uses forward slashes (DS is a backslash on Windows)
  697. $theme['asset_base'] = str_replace(DS, '/', $theme['asset_base']);
  698. return $theme;
  699. }
  700. }