PageRenderTime 65ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/pragwork/modules/Application/Controller.php

https://github.com/szw/pragwork
PHP | 1624 lines | 1146 code | 55 blank | 423 comment | 40 complexity | 498a7fe24576e62ed0dee00fca99ade9 MD5 | raw file
  1. <?php
  2. namespace Application;
  3. /**
  4. * The abstract base class for your controllers.
  5. *
  6. * @author Szymon Wrozynski
  7. * @package Application
  8. */
  9. abstract class Controller extends Singleton
  10. {
  11. /**
  12. * Sets the name of the default layout for templates of this {@link Controller}. All layouts are placed under
  13. * the 'layouts' subdirectory. A layout may be placed in its own subdirectory as well.
  14. * The subdirectories are separated with a backslash <code>\</code>. If there is a backslash in the specified
  15. * value then the given value is treated as a path starting from the 'views' directory.
  16. * The backslash should not be a first character.
  17. *
  18. * Examples:
  19. *
  20. * <code>
  21. * class MyController extends \Application\Controller
  22. * {
  23. * static $layout = 'main'; # views/layouts/main.php
  24. * static $layout = '\Admin\main'; # bad!
  25. * static $layout = 'Admin\main'; # views/layouts/Admin/main.php
  26. * }
  27. * </code>
  28. *
  29. * The layout(s) may be specified with modifiers:
  30. *
  31. * <ul>
  32. * <li><b>only</b>: the layout will be used only for the specified
  33. * action(s)</li>
  34. * <li><b>except</b>: the layout will be used for everything but the
  35. * specified action(s)</li>
  36. * </ul>
  37. *
  38. * Example:
  39. *
  40. * <code>
  41. * class MyController extends \Application\Controller
  42. * {
  43. * static $layout = array(
  44. * array('custom', 'only' => array('index', 'show')),
  45. * 'main'
  46. * );
  47. * # ...
  48. * }
  49. * </code>
  50. *
  51. * In the example above, the <code>custom</code> layout is used only for <code>index</code> and <code>show</code>
  52. * actions. For all other actions the <code>main</code> layout is used.
  53. *
  54. * All layout entries are evaluated in the order defined in the array until the first matching layout.
  55. *
  56. * @var mixed
  57. */
  58. static $layout = null;
  59. /**
  60. * Sets public filter methods to run before firing the requested action.
  61. *
  62. * According to the general Pragwork rule, single definitions may be kept as strings whereas the compound ones
  63. * should be expressed within arrays.
  64. *
  65. * Filters can be extended or modified by class inheritance. The filters defined in a subclass can alter
  66. * the modifiers of the superclass. Filters are fired from the superclass to the subclass.
  67. * If a filter method returns a boolean false then the filter chain execution is stopped.
  68. *
  69. * <code>
  70. * class MyController extends \Application\Controller
  71. * {
  72. * $before_filter = 'init';
  73. *
  74. * # ...
  75. * }
  76. * </code>
  77. *
  78. * There are two optional modifiers:
  79. *
  80. * <ul>
  81. * <li><b>only</b>: action or an array of actions that trigger off the filter</li>
  82. * <li><b>except</b>: action or an array of actions excluded from the filter triggering</li>
  83. * </ul>
  84. *
  85. * <code>
  86. * class MyController extends \Application\Controller
  87. * {
  88. * static $before_filter = array(
  89. * 'alter_breadcrumbs',
  90. * 'except' => 'write_to_all'
  91. * );
  92. *
  93. * # ...
  94. * }
  95. * </code>
  96. *
  97. * <code>
  98. * class ShopController extends \Application\Controller
  99. * {
  100. * static $before_filter = array(
  101. * array('redirect_if_no_payments', 'except' => 'index'),
  102. * array('convert_floating_point',
  103. * 'only' => array('create', 'update'))
  104. * );
  105. *
  106. * # ...
  107. * }
  108. * </code>
  109. *
  110. * @see $before_render_filter
  111. * @see $after_filter
  112. * @see $exception_filter
  113. * @var mixed
  114. */
  115. static $before_filter = null;
  116. /**
  117. * Sets public filter methods to run just before the first rendering a view (excluding partials).
  118. *
  119. * According to the general Pragwork rule, single definitions may be kept as strings whereas the compound ones
  120. * should be expressed within arrays.
  121. *
  122. * There are two optional modifiers:
  123. *
  124. * <ul>
  125. * <li><b>only</b>: action or an array of actions that trigger off the filter</li>
  126. * <li><b>except</b>: action or an array of actions excluded from the filter triggering</li>
  127. * </ul>
  128. *
  129. * See {@link $before_filter} for syntax details.
  130. *
  131. * @see $before_filter
  132. * @see $after_filter
  133. * @see $exception_filter
  134. * @var mixed
  135. */
  136. static $before_render_filter = null;
  137. /**
  138. * Sets public filter methods to run after rendering a view.
  139. *
  140. * According to the general Pragwork rule, single definitions may be kept as strings whereas the compound ones
  141. * should be expressed within arrays.
  142. *
  143. * There are two optional modifiers:
  144. *
  145. * <ul>
  146. * <li><b>only</b>: action or an array of actions that trigger off the filter</li>
  147. * <li><b>except</b>: action or an array of actions excluded from the filter triggering</li>
  148. * </ul>
  149. *
  150. * See {@link $before_filter} for syntax details.
  151. *
  152. * @see $before_filter
  153. * @see $before_render_filter
  154. * @see $exception_filter
  155. * @var mixed
  156. */
  157. static $after_filter = null;
  158. /**
  159. * Sets filter methods to run after rendering a view.
  160. *
  161. * Filter methods must be public or protected. The exception is passed as a parameter.
  162. *
  163. * Unlike in other filter definitions, there are also three (not two) optional modifiers:
  164. *
  165. * <ul>
  166. * <li><b>only</b>: action or an array of actions that trigger off the filter</li>
  167. * <li><b>except</b>: action or an array of actions excluded from the filter triggering</li>
  168. * <li><b>exception</b>: the name of the exception class that triggers off the filter</li>
  169. * </ul>
  170. *
  171. * <code>
  172. * class PeopleController extends \Application\Controller
  173. * {
  174. * static $exception_filter = array(
  175. * array(
  176. * 'record_not_found',
  177. * 'exception' => 'ActiveRecord\RecordNotFound'
  178. * ),
  179. * array(
  180. * 'undefined_property',
  181. * 'only' => array('create', 'update'),
  182. * 'exception' => 'ActiveRecord\UndefinedPropertyException'
  183. * )
  184. * );
  185. *
  186. * # ...
  187. * }
  188. * </code>
  189. *
  190. * <code>
  191. * class AddressesController extends \Application\Controller
  192. * {
  193. * static $exception_filter = array(
  194. * 'undefined_property',
  195. * 'only' => array('create', 'update'),
  196. * 'exception' => 'ActiveRecord\UndefinedPropertyException'
  197. * );
  198. *
  199. * # ...
  200. * }
  201. * </code>
  202. *
  203. * If the <code>exception</code> modifier is missed, any exception triggers off the filter method. If the modifier
  204. * is specified, only a class specified as a string triggers off the filter method.
  205. *
  206. * Only a string is allowed to be a value of the <code>exception</code> modifier. This is because of the nature of
  207. * exception handling. Exceptions are most often constructed with the inheritance in mind and they are grouped by
  208. * common ancestors.
  209. *
  210. * The filter usage resembles the <code>try-catch</code> blocks where the single exception types are allowed
  211. * in the <code>catch</code> clause. In fact, the {@link $exception_filter} may be considered as a syntactic sugar
  212. * to <code>try-catch</code> blocks, where the same <code>catch</code> clause may be adopted to different actions.
  213. *
  214. * See {@link $before_filter} for more syntax details.
  215. *
  216. * @see $before_filter
  217. * @see $before_render_filter
  218. * @see $after_filter
  219. * @var mixed
  220. */
  221. static $exception_filter = null;
  222. /**
  223. * Caches the actions using the page-caching approach. The cache is stored within the public directory,
  224. * in a path matching the requested URL. All cached files have the '.html' extension if needed.
  225. * They can be loaded with the help of the <b>mod_rewrite</b> module (Apache) or similar (see the Pragwork default
  226. * .htaccess file). Notice, caching usually requires the 'DirectorySlash Off' directive (<b>mod_dir</b>)
  227. * or equivalent to prevent appending a trailing slash to the path (and automatic redirection) if the web server
  228. * finds a directory named similarly to the cached file.
  229. *
  230. * The parameters in the query string are not cached.
  231. *
  232. * Available options:
  233. *
  234. * <ul>
  235. * <li><b>if</b>: name(s) of (a) callback method(s)</li>
  236. * </ul>
  237. *
  238. * The writing of cache may be prevented by setting a callback method or an array of callback methods in the
  239. * <code>if</code> option. The callback methods are run just before cache writing at the end of the
  240. * {@link $after_filter} chain. If one of them returns a value evaluated to false the writing is not performed.
  241. * You may also prevent caching by redirecting, throwing the {@link StopException}, or interrupting the
  242. * {@link $after_filter} chain.
  243. *
  244. * <code>
  245. * class MyController extends \Application\Controller
  246. * {
  247. * static $caches_page = array(
  248. * array(
  249. * 'page',
  250. * 'post',
  251. * 'if' => array('no_forms', 'not_logged_and_no_flash')
  252. * ),
  253. * 'index',
  254. * 'if' => 'not_logged_and_no_flash'
  255. * );
  256. *
  257. * public function no_forms()
  258. * {
  259. * return (isset($this->page) && !$this->page->form)
  260. * || (isset($this->post) && !$this->post->allow_comments);
  261. * }
  262. *
  263. * public function not_logged_and_no_flash()
  264. * {
  265. * return !$this->session->admin && !$this->flash->notice;
  266. * }
  267. *
  268. * # ...
  269. * }
  270. * </code>
  271. *
  272. * Notice that caching is working properly only if the cache is enable in the configuration
  273. * (see {@link Configuration}).
  274. *
  275. * @see expire_page()
  276. * @see $caches_action
  277. * @see Configuration
  278. * @var mixed String or array containing action name(s)
  279. */
  280. static $caches_page = null;
  281. /**
  282. * Caches the actions views and stores cache files in the 'temp' directory. Action caching differs from page
  283. * caching because action caching always runs {@link $before_filter}(s).
  284. *
  285. * Available options:
  286. *
  287. * <ul>
  288. * <li><b>if</b>: name(s) of (a) callback method(s)</li>
  289. * <li><b>cache_path</b>: custom cache path</li>
  290. * <li><b>expires_in</b>: expiration time for the cached action in seconds</li>
  291. * <li><b>layout</b>: set to false to cache the action only (without the default layout)</li>
  292. * </ul>
  293. *
  294. * The writing of cache may be prevented by setting a callback method or an array of callback methods in the
  295. * <code>if</code> option. The callback methods are run just before cache writing at the end of the
  296. * {@link $after_filter} chain. If one of them returns a value evaluated to false the writing is not performed.
  297. * You may also prevent caching by redirecting, throwing the {@link StopException}, or interrupting the
  298. * {@link $after_filter} chain.
  299. *
  300. * The <code>cache_path</code> option may be a string started from <code>/</code> or just a name of a method
  301. * returning the path. If a method is used, then its returned value should be a string beginning with the
  302. * <code>/</code> or a full URL (starting with 'http') as returned by the {@link url_for()} function.
  303. *
  304. * <code>
  305. * class MyController extends \Application\Controller
  306. * {
  307. * static $caches_action = array(
  308. * 'edit',
  309. * 'cache_path' => 'edit_cache_path'
  310. * );
  311. *
  312. * public function edit_cache_path()
  313. * {
  314. * return url_for(array(
  315. * 'edit',
  316. * 'params' => $this->params->only('lang')
  317. * ));
  318. * }
  319. *
  320. * # ...
  321. * }
  322. * </code>
  323. *
  324. * Notice that caching is working properly only if the cache is enable in the configuration
  325. * (see {@link Configuration}).
  326. *
  327. * @see expire_action()
  328. * @see $caches_page
  329. * @var mixed String or array with action name(s)
  330. */
  331. static $caches_action = null;
  332. /**
  333. * The {@link Parameters} object that contains request parameters.
  334. *
  335. * @see Parameters
  336. * @var Parameters
  337. */
  338. public $params;
  339. /**
  340. * The {@link Session} object that simplifies the session usage or null if sessions are disabled.
  341. *
  342. * @see Session
  343. * @see Configuration
  344. * @var Session
  345. */
  346. public $session;
  347. /**
  348. * The {@link Cookies} object that simplifies handling cookies.
  349. *
  350. * @see Cookies
  351. * @var Cookies
  352. */
  353. public $cookies;
  354. /**
  355. * The {@link Flash} object for easy handling flash messages or null if sessions are disabled.
  356. *
  357. * @see Flash
  358. * @see Session
  359. * @var Flash
  360. */
  361. public $flash;
  362. /**
  363. * The {@link Request} object storing data and informative methods about the current request.
  364. *
  365. * @see Request
  366. * @var Request
  367. */
  368. public $request;
  369. /**
  370. * The {@link Response} object storing data about the request response being generated.
  371. *
  372. * @see Response
  373. * @var Response
  374. */
  375. public $response;
  376. /**
  377. * The name of the current action.
  378. *
  379. * @var string
  380. */
  381. public $action;
  382. /**
  383. * The simplified name of the current controller (e.g. 'Admin\People', 'Index', etc.).
  384. *
  385. * @var string
  386. */
  387. public $controller;
  388. protected static $instance;
  389. private static $_content;
  390. private static $_ch_file;
  391. private static $_ch_dir;
  392. private static $_ch_layout;
  393. private static $_ch_if;
  394. private static $_rendered;
  395. private static $_http_url;
  396. private static $_ssl_url;
  397. private static $_cache;
  398. protected function __construct()
  399. {
  400. $config = Configuration::instance();
  401. self::$_http_url = $config->http_url();
  402. self::$_ssl_url = $config->ssl_url();
  403. self::$_cache = $config->cache;
  404. $this->params = Parameters::instance();
  405. $this->session = Session::instance();
  406. $this->flash = Flash::instance();
  407. $this->cookies = Cookies::instance();
  408. $class = get_class($this);
  409. do
  410. {
  411. require HELPERS . str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 12, -10)) . 'Helper.php';
  412. }
  413. while (($class = get_parent_class($class)) !== __CLASS__);
  414. }
  415. /**
  416. * The method name reserved for testing environment
  417. */
  418. private final function clean_up() {}
  419. /**
  420. * Starts request processing. It starts the the proper action depending on the {@link Request} object.
  421. * It is called automatically by the {@link Application\start()} function.
  422. *
  423. * @param Request $request The Request object instance
  424. * @internal
  425. */
  426. public function process($request, $response)
  427. {
  428. $this->request = $request;
  429. $this->response = $response;
  430. $this->action = $action = $request->route['action'];
  431. $this->controller = $request->route['controller'];
  432. try
  433. {
  434. ob_start();
  435. $this->invoke_filters('before_filter');
  436. if (self::$_cache)
  437. {
  438. if (static::$caches_action)
  439. $this->set_action_cache($request->uri());
  440. if (static::$caches_page)
  441. $this->set_page_cache($request->uri());
  442. }
  443. $this->$action();
  444. if (!self::$_rendered)
  445. $this->render();
  446. $this->response->body = ob_get_clean();
  447. $this->invoke_filters('after_filter');
  448. }
  449. catch (StopException $e)
  450. {
  451. $this->response->body = ob_get_clean();
  452. }
  453. catch (\Exception $e)
  454. {
  455. if ($this->invoke_filters('exception_filter', $e) !== false)
  456. {
  457. $this->response->body = ob_get_clean();
  458. $this->response->status = 500;
  459. $this->response->exception = $e;
  460. }
  461. }
  462. }
  463. /**
  464. * Returns the default URL options used by all functions based on the {@link url_for()} function.
  465. * This method should return an array of default options or nothing (a null value).
  466. * Each default option may be overridden by each call to the {@link url_for()} function.
  467. *
  468. * @see url_for()
  469. * @return array Default URL options or null
  470. */
  471. public function default_url_options() {}
  472. /**
  473. * Deletes the cached page, cached via the {@link $caches_page}.
  474. *
  475. * The cache path is computed from options passed internally to the {@link url_for()} function.
  476. * Therefore see the {@link url_for()} function for options syntax.
  477. *
  478. * @see $caches_page
  479. * @param mixed $options Options array or action name (string)
  480. */
  481. public function expire_page($options=array())
  482. {
  483. $key = url_for($options);
  484. $qpos = strpos($key, '?');
  485. $start = strlen(($key[4] === 's') ? self::$_ssl_url : self::$_http_url);
  486. $key = ($qpos === false)
  487. ? trim(substr($key, $start), '/.')
  488. : trim(substr($key, $start, $qpos - $start), '/.');
  489. if (isset($key[0]))
  490. {
  491. $cached_page = str_replace('/', DIRECTORY_SEPARATOR, $key);
  492. if (substr($key, -5) !== '.html')
  493. $cached_page .= '.html';
  494. }
  495. else
  496. $cached_page = 'index.html';
  497. if (is_file($cached_page))
  498. {
  499. unlink($cached_page);
  500. while (($dir = dirname($cached_page)) !== '.')
  501. {
  502. $empty = true;
  503. $handler = opendir($dir);
  504. while (false !== ($file = readdir($handler)))
  505. {
  506. if (($file !== '.') && ($file !== '..'))
  507. {
  508. $empty = false;
  509. break;
  510. }
  511. }
  512. closedir($handler);
  513. if (!$empty)
  514. break;
  515. rmdir($dir);
  516. $cached_page = $dir;
  517. }
  518. }
  519. }
  520. /**
  521. * Deletes the cached action view, cached via the {@link $caches_action}.
  522. *
  523. * The cache path is computed from options passed internally to the {@link url_for()} function.
  524. * Therefore see the {@link url_for()} function for options syntax.
  525. *
  526. * @see $caches_action
  527. * @param mixed $options Options array or action name (string)
  528. */
  529. public function expire_action($options=array())
  530. {
  531. $key = url_for($options);
  532. $key = substr($key, strlen(($key[4]==='s') ? self::$_ssl_url : self::$http_url));
  533. # if longer than 1 char (e.g. longer than '/')
  534. if (isset($key[1]))
  535. $key = rtrim($key, '/.');
  536. $cached_dir = TEMP . 'ca_' . md5($key);
  537. if (is_dir($cached_dir))
  538. {
  539. $handler = opendir($cached_dir);
  540. while (false !== ($file = readdir($handler)))
  541. {
  542. if ($file[0] !== '.')
  543. unlink($cached_dir . DIRECTORY_SEPARATOR . $file);
  544. }
  545. closedir($handler);
  546. rmdir($cached_dir);
  547. }
  548. $cached_action = $cached_dir . '.cache';
  549. if (is_file($cached_action))
  550. unlink($cached_action);
  551. }
  552. /**
  553. * Returns true if the cached fragment is available.
  554. *
  555. * The cache path is computed from options passed internally to the {@link url_for()} function.
  556. * Therefore see the {@link url_for()} function for options syntax.
  557. * Moreover, the additional following options are available:
  558. *
  559. * <ul>
  560. * <li><b>action_suffix</b>: the path suffix allowing many fragments in the same action</li>
  561. * </ul>
  562. *
  563. * @see cache()
  564. * @see expire_fragment()
  565. * @param mixed $options Options array or action name (string)
  566. * @return bool True if the fragment exists, false otherwise
  567. */
  568. public function fragment_exists($options=array())
  569. {
  570. return is_file(self::fragment_file($options));
  571. }
  572. /**
  573. * Deletes a cached fragment.
  574. *
  575. * The cache path is computed from options passed internally to the {@link url_for()} function.
  576. * Therefore see the {@link url_for()} function for options syntax.
  577. * Moreover, the additional following options are available:
  578. *
  579. * <ul>
  580. * <li><b>action_suffix</b>: the path suffix allowing many fragments in the same action</li>
  581. * </ul>
  582. *
  583. * @see cache()
  584. * @see fragment_exists()
  585. * @param mixed $options Options array or action name (string)
  586. */
  587. public function expire_fragment($options=array())
  588. {
  589. $fragment = self::fragment_file($options);
  590. if (is_file($fragment))
  591. unlink($fragment);
  592. }
  593. /**
  594. * Caches the fragment enclosed in the closure.
  595. *
  596. * The cache path is computed from options passed internally to the {@link url_for()} function.
  597. * Therefore see the {@link url_for()} function for options syntax.
  598. * Moreover, the additional following options are available:
  599. *
  600. * <ul>
  601. * <li><b>action_suffix</b>: the suffix allowing many fragments in the same action</li>
  602. * <li><b>expires_in</b>: time-to-live for the cached fragment (in sec)</li>
  603. * </ul>
  604. *
  605. * Notice, this method will write and read cached fragments only if the cache is enable in the configuration
  606. * (see {@link Configuration}).
  607. *
  608. * @see expire_fragment()
  609. * @see fragment_exists()
  610. * @param mixed $options Options array or action name (string)
  611. * @param \Closure $closure Content to be cached and displayed
  612. */
  613. public function cache($options, $closure)
  614. {
  615. if (!self::$_cache)
  616. return $closure($this);
  617. $frag = self::fragment_file($options);
  618. if (is_file($frag))
  619. {
  620. if (isset($options['expires_in']))
  621. {
  622. if ((filemtime($frag) + $options['expires_in']) > $_SERVER['REQUEST_TIME'])
  623. return readfile($frag);
  624. }
  625. else
  626. return readfile($frag);
  627. }
  628. ob_start();
  629. $closure($this);
  630. $output = ob_get_clean();
  631. file_put_contents($frag, $output);
  632. echo $output;
  633. }
  634. private static final function fragment_file($options)
  635. {
  636. $key = url_for($options);
  637. $key = substr($key,strlen(($key[4]==='s') ? self::$_ssl_url : self::$_http_url));
  638. if (isset($options['action_suffix']))
  639. $key .= $options['action_suffix'];
  640. return TEMP . 'cf_' . md5($key) . '.cache';
  641. }
  642. private final function render_cache_in_layout()
  643. {
  644. if (is_dir(self::$_ch_dir))
  645. {
  646. $handler = opendir(self::$_ch_dir);
  647. while (false !== ($file = readdir($handler)))
  648. {
  649. if ($file[0] !== '.')
  650. self::$_content[$file] = file_get_contents(self::$_ch_dir . DIRECTORY_SEPARATOR . $file);
  651. }
  652. closedir($handler);
  653. }
  654. $this->render(array('text' => file_get_contents(self::$_ch_file), 'layout' => true));
  655. throw new StopException;
  656. }
  657. private final function set_action_cache($key)
  658. {
  659. if (self::$_ch_file)
  660. return;
  661. foreach (self::normalize_defs(static::$caches_action) as $ch)
  662. {
  663. if ($ch[0] !== $this->action)
  664. continue;
  665. if (isset($ch['cache_path']))
  666. {
  667. if ($ch['cache_path'][0] === '/')
  668. self::$_ch_dir = TEMP . 'ca_' . md5($ch['cache_path']);
  669. else
  670. {
  671. $chp = $this->$ch['cache_path']();
  672. if ($chp[0] === '/')
  673. self::$_ch_dir = TEMP . 'ca_' . md5($chp);
  674. elseif ($chp[4] === 's')
  675. self::$_ch_dir = TEMP . 'ca_' . md5(substr($chp, strlen(self::$_ssl_url)));
  676. else
  677. self::$_ch_dir = TEMP . 'ca_' . md5(substr($chp, strlen(self::$_http_url)));
  678. }
  679. }
  680. else
  681. self::$_ch_dir = TEMP . 'ca_' . md5(isset($key[1]) ? rtrim($key, '/.') : $key);
  682. self::$_ch_file = self::$_ch_dir . '.cache';
  683. if (isset($ch['layout']) && !$ch['layout'])
  684. {
  685. if (is_file(self::$_ch_file))
  686. {
  687. if (isset($ch['expires_in']))
  688. {
  689. if ((filemtime(self::$_ch_file)+$ch['expires_in'])>$_SERVER['REQUEST_TIME'])
  690. $this->render_cache_in_layout();
  691. }
  692. else
  693. $this->render_cache_in_layout();
  694. }
  695. self::$_ch_layout = self::resolve_layout($this->action);
  696. static::$layout = null;
  697. }
  698. elseif (is_file(self::$_ch_file))
  699. {
  700. if (isset($ch['expires_in']))
  701. {
  702. if ((filemtime(self::$_ch_file) + $ch['expires_in']) > $_SERVER['REQUEST_TIME'])
  703. {
  704. readfile(self::$_ch_file);
  705. throw new StopException;
  706. }
  707. }
  708. else
  709. {
  710. readfile(self::$_ch_file);
  711. throw new StopException;
  712. }
  713. }
  714. if (isset($ch['if']))
  715. self::$_ch_if = $ch['if'];
  716. self::add_to_filter('after_filter', 'write_and_show_cache');
  717. break;
  718. }
  719. }
  720. private final function set_page_cache($key)
  721. {
  722. if (self::$_ch_file)
  723. return;
  724. foreach (self::normalize_defs(static::$caches_page) as $ch)
  725. {
  726. if ($ch[0] !== $this->action)
  727. continue;
  728. $key = trim($key, '/.');
  729. if (isset($key[0]))
  730. {
  731. self::$_ch_file = str_replace('/', DIRECTORY_SEPARATOR, $key);
  732. if (!strpos($key, '.'))
  733. self::$_ch_file .= '.html';
  734. }
  735. else
  736. self::$_ch_file = 'index.html';
  737. if (isset($ch['if']))
  738. self::$_ch_if = $ch['if'];
  739. self::add_to_filter('after_filter', 'write_and_show_cache');
  740. break;
  741. }
  742. }
  743. private final function write_and_show_cache()
  744. {
  745. if (self::$_ch_if)
  746. {
  747. if ((array) self::$_ch_if === self::$_ch_if)
  748. {
  749. foreach (self::$_ch_if as $ifm)
  750. {
  751. if (!$this->$ifm())
  752. return;
  753. }
  754. }
  755. else
  756. {
  757. $ifm = self::$_ch_if;
  758. if (!$this->$ifm())
  759. return;
  760. }
  761. }
  762. if (!self::$_ch_dir) # if caches page
  763. {
  764. $dir = dirname(self::$_ch_file);
  765. if (!is_dir($dir))
  766. mkdir($dir, 0775, true);
  767. }
  768. file_put_contents(self::$_ch_file, $this->response->body);
  769. if (self::$_ch_layout)
  770. {
  771. if (self::$_content)
  772. {
  773. if (is_dir(self::$_ch_dir))
  774. {
  775. $handler = opendir(self::$_ch_dir);
  776. while (false !== ($f = readdir($handler)))
  777. {
  778. if ($f[0] !== '.')
  779. unlink(self::$_ch_dir .DIRECTORY_SEPARATOR .$f);
  780. }
  781. closedir($handler);
  782. }
  783. else
  784. mkdir(self::$_ch_dir, 0775);
  785. foreach (self::$_content as $r => $c)
  786. file_put_contents(self::$_ch_dir.DIRECTORY_SEPARATOR.$r,$c);
  787. }
  788. ob_start();
  789. $this->render(array('text' => $this->response->body, 'layout' => self::$_ch_layout));
  790. $this->response->body = ob_get_clean();
  791. }
  792. }
  793. /**
  794. * Sends a location in the HTTP header causing a HTTP client to redirect.
  795. *
  796. * The location URL is obtained from {@link url_for()} function. See {@link url_for()} function for options syntax.
  797. *
  798. * Additionally, following options are available:
  799. *
  800. * <ul>
  801. * <li><b>status</b>: HTTP status code (default: 302). It might be an int or a symbolic name (e.g. 'found')</li>
  802. * <li><b>[name]</b>: additional flash message</li>
  803. * </ul>
  804. *
  805. * Example:
  806. *
  807. * <code>
  808. * $this->redirect_to(array('index', 'notice' => 'Post updated.'));
  809. * # Redirects to the action 'index' and sets the appropriate flash
  810. * # message.
  811. * </code>
  812. *
  813. * @see url_for()
  814. * @see redirect_to_url()
  815. * @param mixed $options Options array or action name (string)
  816. * @throws {@link StopException} In order to stop further execution
  817. */
  818. public function redirect_to($options=array())
  819. {
  820. if ((array) $options !== $options)
  821. $this->redirect_to_url(url_for($options));
  822. elseif (!$this->session)
  823. $this->redirect_to_url(url_for($options), $options);
  824. $url = url_for($options);
  825. unset(
  826. $options['params'],
  827. $options[0],
  828. $options['name'],
  829. $options['ssl'],
  830. $options['anchor'],
  831. $options['locale'],
  832. $options['action'],
  833. $options['controller']
  834. );
  835. $this->redirect_to_url($url, $options);
  836. }
  837. /**
  838. * Sends a location in the HTTP header causing a HTTP client to redirect.
  839. *
  840. * The following options are available:
  841. *
  842. * <ul>
  843. * <li><b>status</b>: HTTP status code (default: 302) It might be an int or a symbolic name (e.g. 'found')</li>
  844. * <li><b>[name]</b>: additional flash message</li>
  845. * </ul>
  846. *
  847. * @see redirect_to()
  848. * @param string $url URL of the location
  849. * @param mixed $options Options array
  850. * @throws {@link StopException} In order to stop further execution
  851. */
  852. public function redirect_to_url($url, $options=array())
  853. {
  854. if (isset($options['status']))
  855. {
  856. $this->response->status = $options['status'];
  857. unset($options['status']);
  858. }
  859. else
  860. $this->response->status = 302;
  861. if ($this->flash)
  862. {
  863. foreach ($options as $name => $msg)
  864. $this->flash->$name = $msg;
  865. }
  866. $this->response->location = $url;
  867. throw new StopException;
  868. }
  869. /**
  870. * Renders a template depending on parameters passed via $options.
  871. *
  872. * The rendering topic may be divided into two big areas of use: rendering of templates and rendering of partial
  873. * templates.
  874. *
  875. * <h4>A. General rendering of templates</h4>
  876. *
  877. * Templates are placed under the directory 'views' of the application code. They fill the directory structure
  878. * according to the controllers structure.
  879. *
  880. * <h4>A.1. Rendering a template in the current controller</h4>
  881. *
  882. * <code>
  883. * class ShopController extends \Application\Controller
  884. * {
  885. * public function index()
  886. * {
  887. * # $this->render();
  888. * }
  889. *
  890. * public function show()
  891. * {
  892. * $this->render('index');
  893. * $this->render(array('index'));
  894. * $this->render(arary('action' => 'index'));
  895. * }
  896. * }
  897. * </code>
  898. *
  899. * Each of the method calls in the action <code>show</code> above renders the template of the action
  900. * <code>index</code> of the current controller. However you should render a template once.
  901. * Notice also, rendering the template does not stop the further action execution.
  902. *
  903. * The {@link render()} with no arguments renders the template of the current action.
  904. * But the action renders its own template implicitly if no other render is used (except partial templates).
  905. * Therefore there is no need to explicit call {@link render()} in the <code>index</code> action above.
  906. *
  907. * <h4>A.2. Rendering a template of another controller</h4>
  908. *
  909. * <code>
  910. * class CustomersController extends \Application\Controller
  911. * {
  912. * public function edit() {}
  913. * }
  914. *
  915. * class ShopController extends \Application\Controller
  916. * {
  917. * public function show()
  918. * {
  919. * $this->render(array(
  920. * 'action' => 'edit',
  921. * 'controller' => 'Customers'
  922. * ));
  923. *
  924. * # or just: $this->render('Customers\edit')
  925. * }
  926. * }
  927. * </code>
  928. *
  929. * Renders the template of the <code>edit</code> action of the Customers controller.
  930. *
  931. * <h4>A.3. Rendering a custom template</h4>
  932. *
  933. * <code>
  934. * $this->render('my_custom_template');
  935. * $this->render('my_templates\my_custom_template');
  936. * $this->render(array('my_custom_template', 'layout' => false))
  937. * $this->render(array('my_custom_template', 'layout' => 'my_layout'));
  938. * </code>
  939. *
  940. * Renders a custom template. The custom template may be placed in a subdirectory.
  941. * The subdirectories are separated with a backslash <code>\</code>. If there is a backslash in the string,
  942. * the path starts from the root (the 'views' directory).
  943. *
  944. * Each of the examples from the part <b>A</b> can be altered with an option <code>layout</code> which can point
  945. * to a certain {@link $layout}. Also, this option can be set to <code>false</code> disabling the global
  946. * layout defined in the controller. The layout file should be put in the 'views/layouts' subdirectory.
  947. *
  948. * <code>
  949. * class ShopController extends \Application\Controller
  950. * {
  951. * static $layout = 'shop';
  952. *
  953. * public function index()
  954. * {
  955. * # use the default layout ('views/layouts/shop.php')
  956. * }
  957. *
  958. * public function show()
  959. * {
  960. * $this->render(array('layout' => false));
  961. *
  962. * # do not use any layout, same as self::$layout = null;
  963. * }
  964. *
  965. * public function edit()
  966. * {
  967. * $this->render(array(
  968. * 'action' => 'show',
  969. * 'layout' => 'custom_layout'
  970. * ));
  971. *
  972. * # or just:
  973. * # $this->render(array('show', 'layout' => 'custom_layout'));
  974. *
  975. * # use the template 'views/Shop/show.php' with
  976. * # the layout 'views/layouts/custom_layout.php'.
  977. * }
  978. * }
  979. * </code>
  980. *
  981. * <h4>A.4. Content Format and Status</h4>
  982. *
  983. * It is possible to specify a content format in the header sended with the first use of the {@link render()}
  984. * method (excluding partials). It can be done with help of the <code>content_type</code> option.
  985. *
  986. * <code>
  987. * $this->render(array('show', 'content_type' => 'text/xml'));
  988. * </code>
  989. *
  990. * Also, you can set a status for your content:
  991. *
  992. * <code>
  993. * $this->render(array('status' => 200)); # 200 OK
  994. * $this->render(array('status' => 'created')); # 201 Created
  995. * </code>
  996. *
  997. * If a status denotes the client error (400 - 499) and there is no template to selected explicitly to render
  998. * then the error template (from 'errors' directory) is rendered. For server errors (greater or equal than 500),
  999. * the special template (500.php) is rendered.
  1000. *
  1001. * <code>
  1002. * $this->render(array('status' => 'not_found')); # renders errors/404.php
  1003. * $this->render(array('status' => 405)); #renders errors/405.php
  1004. * </code>
  1005. *
  1006. * <h4>A.5. Format</h4>
  1007. *
  1008. * Format enables additional headers to be sent on the first call of {@link render()} (again partials does not
  1009. * count). Also, it provides additional, specific behavior, depending on the chosen format.
  1010. *
  1011. * Currently there is only one format available: <b>xml</b>.
  1012. *
  1013. * <code>
  1014. * $this->render(array('format' => 'xml'));
  1015. * </code>
  1016. *
  1017. * It renders the template (the default one in this particular example) with the header: "Content-Type: text/xml;
  1018. * charset=utf-8" (no content format was specified). Moreover, it disables the global layout. You can always use
  1019. * a layout by specifying a layout template:
  1020. *
  1021. * <code>
  1022. * $this->render(array('format' => 'xml', 'layout' => 'my_xml_layout'));
  1023. * </code>
  1024. *
  1025. * Or, you can turn on the global layout by setting <code>layout</code> to <code>true</code> explicitly.
  1026. *
  1027. * <h4>A.6. Text, XML, JSON</h4>
  1028. *
  1029. * You can also specify a text (or xml, json) instead of a template.
  1030. * It is useful especially in the AJAX applications.
  1031. *
  1032. * <code>
  1033. * $this->render(array('text' => 'OK'));
  1034. * $this->render(array('xml' => $xml));
  1035. * $this->render(array('json' => array('a' => 1, 'b' => 2, 'c' => 3)));
  1036. * </code>
  1037. *
  1038. * If you do not set the custom content format, the 'application/xml' is used for XML and 'application/json'
  1039. * is used for JSON.
  1040. *
  1041. * The <code>json</code> option allows to pass <code>json_options</code> bitmask, just like it is done
  1042. * in the global <code>json_encode()</code> function.
  1043. *
  1044. * <code>
  1045. * $this->render(array(
  1046. * 'json' => array(array(1, 2, 3)),
  1047. * 'json_options' => JSON_FORCE_OBJECT
  1048. * ));
  1049. * </code>
  1050. *
  1051. *
  1052. * <h4>B. Rendering partial templates</h4>
  1053. *
  1054. * Partial templates are placed in the same directory structure as normal templates. They differ from the normal
  1055. * ones in extensions. Partial templates ends with the '.part.php' extension.
  1056. *
  1057. * Whereas normal rendering of templates is taking place in the controller, the rendering of partials is the domain
  1058. * of template files mainly. Usually partial templates represent repetitive portions of code used to construct more
  1059. * compound structures. The result of rendering the partial template is returned as a string - it is not
  1060. * displayed immediately and therefore it should be displayed explicitly with the <code>echo</code> function.
  1061. *
  1062. * If the '.part.php' file is not found the '.php' one is used instead and the template is rendered in the normal
  1063. * way described in the section A.
  1064. *
  1065. * <h4>B.1. Rendering the partial template</h4>
  1066. *
  1067. * <code>
  1068. * <?php echo $this->render('item') ?>
  1069. * </code>
  1070. *
  1071. * The code above renders the partial template 'item.part.php' placed under the controller's directory in the views
  1072. * structure. If the partial template name contains a backslash <code>\</code> the absolute path will be used
  1073. * (with the root set to 'views' directory).
  1074. *
  1075. * <code>
  1076. * <?php echo $this->render('shared\header') ?>
  1077. * # renders /views/shared/header.part.php
  1078. * </code>
  1079. *
  1080. * Everything (except <code>collection</code>) passed as named array elements are converted to local variables
  1081. * inside the partial template.
  1082. *
  1083. * <code>
  1084. * <?php echo $this->render(array('item', 'text' => 'Hello')) ?>
  1085. * # renders the partial template ('item.part.php') and creates a local
  1086. * # variable named $text there.
  1087. * </code>
  1088. *
  1089. * <h4>B.2. Rendering a partial template with a collection</h4>
  1090. *
  1091. * If you use the <code>collection</code> option you can render the partial template a few times, according to items
  1092. * passed in an array as the <code>collection</code>. The current item from the collection is named after
  1093. * the template name, and the array key name has the <code>'_key'</code> suffix.
  1094. *
  1095. * The code below:
  1096. *
  1097. * <code>
  1098. * <?php $this->render(array(
  1099. * 'person',
  1100. * 'collection' => array('John', 'Frank'),
  1101. * 'message' => 'The message.'
  1102. * )) ?>
  1103. * </code>
  1104. *
  1105. * could be used in the 'person.part.php' like here:
  1106. *
  1107. * <code>
  1108. * <h1><?php echo Hello $person ?></h1>
  1109. * <p><?php echo $message ?></p>
  1110. * <p>And the current key is: <?php echo $person_key ?></p>
  1111. * </code>
  1112. *
  1113. * In the above example the 'person.part.php' will be rendered twice, with different names (<code>$person</code>)
  1114. * and keys (<code>$person_key</code>). The whole collection will be still available under
  1115. * the <code>$collection</code> variable.
  1116. *
  1117. * @see $layout
  1118. * @param mixed $options Options array or string
  1119. * @return mixed Rendered partial template or null
  1120. */
  1121. public function render($options=array())
  1122. {
  1123. if ((array) $options !== $options)
  1124. {
  1125. $template = VIEWS . ((strpos($options, '\\') === false)
  1126. ? str_replace('\\', DIRECTORY_SEPARATOR, $this->controller) . DIRECTORY_SEPARATOR . $options
  1127. : str_replace('\\', DIRECTORY_SEPARATOR, $options));
  1128. $partial = $template . '.part.php';
  1129. if (is_file($partial))
  1130. {
  1131. ob_start();
  1132. require $partial;
  1133. return ob_get_clean();
  1134. }
  1135. if (!self::$_rendered)
  1136. {
  1137. $this->invoke_filters('before_render_filter');
  1138. self::$_rendered = true;
  1139. }
  1140. $layout = self::resolve_layout($this->action);
  1141. if ($layout)
  1142. {
  1143. ob_start();
  1144. require $template . '.php';
  1145. self::$_content[0] = ob_get_clean();
  1146. require VIEWS . 'layouts' . DIRECTORY_SEPARATOR
  1147. . str_replace('\\', DIRECTORY_SEPARATOR, $layout) . '.php';
  1148. }
  1149. else
  1150. require $template . '.php';
  1151. return;
  1152. }
  1153. elseif (isset($options[0]))
  1154. {
  1155. $template = VIEWS . ((strpos($options[0], '\\') === false)
  1156. ? str_replace('\\', DIRECTORY_SEPARATOR, $this->controller) . DIRECTORY_SEPARATOR . $options[0]
  1157. : str_replace('\\', DIRECTORY_SEPARATOR, $options[0]));
  1158. $partial = $template . '.part.php';
  1159. if (is_file($partial))
  1160. {
  1161. unset($options[0]);
  1162. ob_start();
  1163. if (isset($options['collection']))
  1164. {
  1165. $name = basename($partial, '.part.php');
  1166. $key_name = $name . '_key';
  1167. foreach ($options['collection'] as $key => $item)
  1168. $this->render_partial($partial, array($name => $item, $key_name => $key) + $options);
  1169. }
  1170. else
  1171. $this->render_partial($partial, $options);
  1172. return ob_get_clean();
  1173. }
  1174. }
  1175. elseif (isset($options['xml']))
  1176. {
  1177. if (!isset($options['content_type']))
  1178. $options['content_type'] = 'application/xml';
  1179. $options['text'] = $xml;
  1180. }
  1181. elseif (isset($options['json']))
  1182. {
  1183. if (!isset($options['content_type']))
  1184. $options['content_type'] = 'application/json';
  1185. $options['text'] = isset($options['json_options'])
  1186. ? json_encode($options['json'], $options['json_options'])
  1187. : json_encode($options['json']);
  1188. }
  1189. elseif (!isset($options['text']))
  1190. $template = VIEWS . str_replace('\\', DIRECTORY_SEPARATOR,
  1191. isset($options['controller']) ? $options['controller'] : $this->controller)
  1192. . DIRECTORY_SEPARATOR . (isset($options['action']) ? $options['action'] : $this->action);
  1193. if (isset($options['status']))
  1194. $this->response->status = $options['status'];
  1195. if (isset($options['text']))
  1196. {
  1197. if (isset($options['content_type']))
  1198. $this->response->content_type = $options['content_type'];
  1199. if (!self::$_rendered)
  1200. {
  1201. $this->invoke_filters('before_render_filter');
  1202. self::$_rendered = true;
  1203. }
  1204. if (isset($options['layout']))
  1205. {
  1206. if ($options['layout'] === true)
  1207. $options['layout'] = self::resolve_layout($this->action);
  1208. self::$_content[0] = $options['text'];
  1209. require VIEWS . 'layouts' . DIRECTORY_SEPARATOR .
  1210. str_replace('\\', DIRECTORY_SEPARATOR, $options['layout']) . '.php';
  1211. }
  1212. else
  1213. echo $options['text'];
  1214. return;
  1215. }
  1216. if (!isset($options[0]) && !isset($options['action']) && !isset($options['controller'])
  1217. && $this->response->status_code() >= 400)
  1218. {
  1219. if (isset($options['content_type']))
  1220. $this->response->content_type = $options['content_type'];
  1221. throw new StopException;
  1222. }
  1223. if (isset($options['format']) && $options['format'] === 'xml')
  1224. {
  1225. if (!isset($options['content_type']))
  1226. $options['content_type'] = 'text/xml';
  1227. if (!isset($options['layout']))
  1228. $options['layout'] = false;
  1229. }
  1230. elseif (!isset($options['layout']))
  1231. $options['layout'] = self::resolve_layout($this->action);
  1232. if (isset($options['content_type']))
  1233. $this->response->content_type = $options['content_type'];
  1234. if (!self::$_rendered)
  1235. {
  1236. $this->invoke_filters('before_render_filter');
  1237. self::$_rendered = true;
  1238. }
  1239. if ($options['layout'])
  1240. {
  1241. ob_start();
  1242. require $template . '.php';
  1243. self::$_content[0] = ob_get_clean();
  1244. require VIEWS . 'layouts' . DIRECTORY_SEPARATOR
  1245. . str_replace('\\', DIRECTORY_SEPARATOR, $options['layout']) . '.php';
  1246. }
  1247. else
  1248. require $template . '.php';
  1249. }
  1250. /**
  1251. * Renders the template just like the {@link render()} method but returns
  1252. * the results as a string.
  1253. *
  1254. * @see render()
  1255. * $param mixed $options Options array or string
  1256. * @return string
  1257. */
  1258. public function render_to_string($options=array())
  1259. {
  1260. ob_start();
  1261. $str = $this->render($options);
  1262. return ob_get_clean() ?: $str;
  1263. }
  1264. private function render_partial($___path___, $___args___)
  1265. {
  1266. foreach ($___args___ as $___n___ => $___v___)
  1267. $$___n___ = $___v___;
  1268. require $___path___;
  1269. }
  1270. /**
  1271. * Returns a rendered template or a template region constructed with the {@link content_for()} method and a name
  1272. * passed as a parameter.
  1273. *
  1274. * This method should be used directly from a layout template. If the region does not exist the null is returned
  1275. * instead.
  1276. *
  1277. * <code>
  1278. * <?php echo $this->yield() ?>
  1279. * </code>
  1280. *
  1281. * <code>
  1282. * <?php echo $this->yield('title') ?>
  1283. * </code>
  1284. *
  1285. * @see content_for()
  1286. * @see render()
  1287. * @param string $region Optional region name
  1288. * @return mixed Rendered template, template region, or null
  1289. */
  1290. public function yield($region=0)
  1291. {
  1292. if (isset(self::$_content[$region]))
  1293. return self::$_content[$region];
  1294. }
  1295. /**
  1296. * Inserts a named content block into a layout view directly from a template.
  1297. *
  1298. * The region name can be used in the layout with the {@link yield()} method. The closure with the content may have
  1299. * an argument. If so, the current controller instance is passed there allowing to get to controller methods and
  1300. * variables.
  1301. *
  1302. * <code>
  1303. * <?php $this->content_for('title', function() { ?>
  1304. * Just simple title
  1305. * <?php }) ?>
  1306. * </code>
  1307. *
  1308. * <code>
  1309. * <?php $this->content_for('title', function($that) { ?>
  1310. *
  1311. * # the current controller is named '$that' by convention
  1312. * # and because '$this' cannot be used in the closure context
  1313. *
  1314. * Records found: <?php echo count($that->records) ?>
  1315. * <?php }) ?>
  1316. * </code>
  1317. *
  1318. * @see yield()
  1319. * @param string $region Region name
  1320. * @param \Closure $closure Content for partial yielding
  1321. */
  1322. public function content_for($region, $closure)
  1323. {
  1324. ob_start();
  1325. $closure($this);
  1326. self::$_content[$region] = ob_get_clean();
  1327. }
  1328. private final function resolve_layout($action)
  1329. {
  1330. if (!static::$layout)
  1331. return;
  1332. static $layout;
  1333. if (!isset($layout))
  1334. {
  1335. $layout = null;
  1336. foreach (self::normalize_defs(static::$layout) as $l)
  1337. {
  1338. if (isset($l['only']))
  1339. {
  1340. if ((((array) $l['only'] === $l['only']) && in_array($action, $l['only'], true))
  1341. || ($l['only'] === $action))
  1342. {
  1343. $layout = $l[0];
  1344. break;
  1345. }
  1346. continue;
  1347. }
  1348. elseif (isset($l['except']) && ((((array) $l['except'] === $l['except'])
  1349. && in_array($action, $l['except'], true)) || ($l['except'] === $action)))
  1350. continue;
  1351. $layout = $l[0];
  1352. break;
  1353. }
  1354. }
  1355. return $layout;
  1356. }
  1357. private static final function get_filter_mods(&$entry)
  1358. {
  1359. $modifiers = array();
  1360. if (isset($entry['only']))
  1361. {
  1362. $modifiers['only'] = ((array) $entry['only'] === $entry['only'])
  1363. ? $entry['only'] : array($entry['only']);
  1364. unset($entry['only']);
  1365. }
  1366. if (isset($entry['except']))
  1367. {
  1368. $modifiers['except'] = ((array) $entry['except'] === $entry['except'])
  1369. ? $entry['except'] : array($entry['except']);
  1370. unset($entry['except']);
  1371. }
  1372. if (isset($entry['exception']))
  1373. {
  1374. $modifiers['exception'] = $entry['exception'];
  1375. unset($entry['exception']);
  1376. }
  1377. return $modifiers;
  1378. }
  1379. private static final function normalize_defs($definitions)
  1380. {
  1381. if ((array) $definitions !== $definitions)
  1382. return array(array($definitions));
  1383. $normalized_definitions = array();
  1384. $outer_options = array();
  1385. foreach ($definitions as $key => $body)
  1386. {
  1387. if ((string) $key === $key)
  1388. $outer_options[$key] = $body;
  1389. elseif ((array) $body === $body)
  1390. {
  1391. $inner_options = array();
  1392. foreach ($body as $k => $v)
  1393. {
  1394. if ((string) $k === $k)
  1395. {
  1396. $inner_options[$k] = $v;
  1397. unset($body[$k]);
  1398. }
  1399. }
  1400. foreach ($body as $b)
  1401. $normalized_definitions[] = array($b) + $inner_options;
  1402. }
  1403. else
  1404. $normalized_definitions[] = array($body);
  1405. }
  1406. if ($outer_options)
  1407. {
  1408. foreach ($normalized_definitions as &$nd)
  1409. $nd += $outer_options;
  1410. }
  1411. return $normalized_definitions;
  1412. }
  1413. private static final function add_to_filter($filter, $method)
  1414. {
  1415. if (!static::$$filter)
  1416. static::$$filter = $method;
  1417. elseif ((array) static::$$filter === static::$$filter)
  1418. {
  1419. if (array_key_exists('except', static::$$filter) || array_key_exists('only', static::$$filter))
  1420. static::$$filter = self::normalize_defs(static::$$filter);
  1421. array_push(static::$$filter, $method);
  1422. }
  1423. else
  1424. static::$$filter = array(static::$$filter, $method);
  1425. }
  1426. private final function invoke_filters($filter, $value=null)
  1427. {
  1428. $filter_chain = array();
  1429. $class = get_class($this);
  1430. do
  1431. {
  1432. $class_filters = $class::$$filter;
  1433. if (!$class_filters)
  1434. continue;
  1435. if ((array) $class_filters !== $class_filters)
  1436. {
  1437. if (!isset($filter_chain[$class_filters]))
  1438. $filter_chain[$class_filters] = null;
  1439. }
  1440. else
  1441. {
  1442. $class_mods = self::get_filter_mods($class_filters);
  1443. foreach (array_reverse($class_filters) as $entry)
  1444. {
  1445. if ((array) $entry !== $entry)
  1446. {
  1447. if (!isset($filter_chain[$entry]))
  1448. $filter_chain[$entry] = $class_mods;
  1449. }
  1450. else
  1451. {
  1452. $mods = self::get_filter_mods($entry);
  1453. foreach (array_reverse($entry) as $e)
  1454. {
  1455. if (!isset($filter_chain[$e]))
  1456. $filter_chain[$e] = $mods ?: $class_mods;
  1457. }
  1458. }
  1459. }
  1460. }
  1461. } while (($class = get_parent_class($class)) !== __CLASS__);
  1462. foreach (array_reverse($filter_chain) as $flt => $mods)
  1463. {
  1464. if (isset($mods['only']) && !in_array($this->action, $mods['only']))
  1465. continue;
  1466. elseif (isset($mods['except']) && in_array($this->action, $mods['except']))
  1467. continue;
  1468. elseif (isset($mods['exception']) && !($value && is_a($value, $mods['exception'])))
  1469. continue;
  1470. if ($this->$flt($value) === false)
  1471. return false;
  1472. }
  1473. }
  1474. }
  1475. ?>