PageRenderTime 116ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/Atomik.php

http://atomikframework.googlecode.com/
PHP | 3021 lines | 1630 code | 376 blank | 1015 comment | 356 complexity | 2f858217e550f90eb3ad1b742b9c91dc MD5 | raw file
Possible License(s): LGPL-2.1, MIT, CC-BY-3.0
  1. <?php
  2. /**
  3. * Atomik Framework
  4. * Copyright (c) 2008-2009 Maxime Bouroumeau-Fuseau
  5. *
  6. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  7. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  8. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  9. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  10. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  11. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  12. * THE SOFTWARE.
  13. *
  14. * @package Atomik
  15. * @author Maxime Bouroumeau-Fuseau
  16. * @copyright 2008-2010 (c) Maxime Bouroumeau-Fuseau
  17. * @license http://www.opensource.org/licenses/mit-license.php
  18. * @link http://www.atomikframework.com
  19. */
  20. define('ATOMIK_VERSION', '2.3');
  21. !defined('ATOMIK_APP_ROOT') && define('ATOMIK_APP_ROOT', './app');
  22. /* -------------------------------------------------------------------------------------------
  23. * APPLICATION CONFIGURATION
  24. * ------------------------------------------------------------------------------------------ */
  25. Atomik::reset(array(
  26. 'app' => array(
  27. /* @var string */
  28. 'default_action' => 'index',
  29. /* The name of the layout
  30. * Add multiple layouts using an array (will be rendered in reverse order)
  31. * @var array|bool|string */
  32. 'layout' => false,
  33. /* @var bool */
  34. 'disable_layout' => false,
  35. /* Whether to propagate view vars to the layout
  36. * @var bool */
  37. 'vars_to_layout' => true,
  38. /* An array where keys are route names and their value is an associative
  39. * array of default values
  40. * @see Atomik::route()
  41. * @var array */
  42. 'routes' => array(),
  43. /* @var bool */
  44. 'force_uri_extension' => false,
  45. /* List of escaping profiles where keys are profile names and their
  46. * value an array of callbacks
  47. * @see Atomik::escape()
  48. * @var array */
  49. 'escaping' => array(
  50. 'default' => array('htmlspecialchars', 'nl2br')
  51. ),
  52. /* @see Atomik::filter()
  53. * @var array */
  54. 'filters' => array(
  55. /* @var array */
  56. 'rules' => array(),
  57. /* @var array */
  58. 'callbacks' => array(),
  59. /* @var string */
  60. 'default_message' => 'The %s field failed to validate',
  61. /* @var string */
  62. 'required_message' => 'The %s field must be filled'
  63. ),
  64. /**
  65. * The callback used to execute actions
  66. * @var callback */
  67. 'executor' => array('Atomik', 'executeFile'),
  68. /* @see Atomik::render()
  69. * @var array */
  70. 'views' => array(
  71. /* @var string */
  72. 'file_extension' => '.phtml',
  73. /* Alternative rendering engine
  74. * @see Atomik::renderFile()
  75. * @var callback */
  76. 'engine' => false,
  77. /* @var string */
  78. 'default_context' => 'html',
  79. /* The GET parameter to retrieve the current context
  80. * @var string */
  81. 'context_param' => 'format',
  82. /* List of contexts where keys are the context name.
  83. * Contexts can specify:
  84. * - prefix (string): the view filename's extension prefix
  85. * - layout (bool): whether the layout should be rendered
  86. * - content_type (string): the HTTP response content type
  87. * @var array */
  88. 'contexts' => array(
  89. 'html' => array(
  90. 'prefix' => '',
  91. 'layout' => true,
  92. 'content_type' => 'text/html'
  93. ),
  94. 'ajax' => array(
  95. 'prefix' => '',
  96. 'layout' => false,
  97. 'content_type' => 'text/html'
  98. ),
  99. 'xml' => array(
  100. 'prefix' => 'xml',
  101. 'layout' => false,
  102. 'content_type' => 'text/xml'
  103. ),
  104. 'json' => array(
  105. 'prefix' => 'json',
  106. 'layout' => false,
  107. 'content_type' => 'application/json'
  108. ),
  109. 'js' => array(
  110. 'prefix' => 'js',
  111. 'layout' => false,
  112. 'content_type' => 'text/javascript'
  113. ),
  114. 'css' => array(
  115. 'prefix' => 'css',
  116. 'layout' => false,
  117. 'content_type' => 'text/css'
  118. )
  119. )
  120. ),
  121. /* A parameter in the route that will allow to specify the http method
  122. * (override the request's method). False to disable
  123. * @var string */
  124. 'http_method_param' => '_method',
  125. /* @var array */
  126. 'allowed_http_methods' => array('GET', 'POST', 'PUT', 'DELETE', 'TRACE', 'HEAD', 'OPTIONS', 'CONNECT')
  127. )
  128. ));
  129. /* -------------------------------------------------------------------------------------------
  130. * CORE CONFIGURATION
  131. * ------------------------------------------------------------------------------------------ */
  132. Atomik::set(array(
  133. /* @var array */
  134. 'plugins' => array(),
  135. /* @var array */
  136. 'atomik' => array(
  137. /* Atomik's filename
  138. * @var string */
  139. 'scriptname' => __FILE__,
  140. /* Base url, set to null for auto detection
  141. * @var string */
  142. 'base_url' => null,
  143. /* Whether url rewriting is activated on the server
  144. * @var bool */
  145. 'url_rewriting' => false,
  146. /* Keep compatibility with 2.2
  147. * @var bool */
  148. 'urimatch_compat' => false,
  149. /* Whether to automatically allow additional params at the end of routed uris
  150. * @var bool */
  151. 'auto_uri_wildcard' => false,
  152. /* @var bool */
  153. 'debug' => false,
  154. /* The GET parameter used to retreive the action
  155. * @var string */
  156. 'trigger' => 'action',
  157. /* Whether to register the class autoloader
  158. * @var bool */
  159. 'class_autoload' => true,
  160. /* @var bool */
  161. 'start_session' => true,
  162. /* @var string */
  163. 'session_namespace' => false,
  164. /* Plugin's assets path template.
  165. * %s will be replaced by the plugin's name
  166. * @see Atomik::pluginAsset()
  167. * @var string */
  168. 'plugin_assets_tpl' => 'app/plugins/%s/assets/',
  169. /* @var array */
  170. 'log' => array(
  171. /* @var bool */
  172. 'register_default' => false,
  173. /* From which level to start logging messages
  174. * @var int */
  175. 'level' => LOG_WARNING,
  176. /* Message template for the default logger
  177. * @see Atomik::logToFile()
  178. * @var string */
  179. 'message_template' => '[%date%] [%level%] %message%'
  180. ),
  181. /* @var array */
  182. 'dirs' => array(
  183. 'app' => ATOMIK_APP_ROOT,
  184. 'plugins' => ATOMIK_APP_ROOT . '/plugins',
  185. 'actions' => ATOMIK_APP_ROOT . '/actions',
  186. 'views' => ATOMIK_APP_ROOT . '/views',
  187. 'layouts' => array(ATOMIK_APP_ROOT . '/views', ATOMIK_APP_ROOT . '/layouts'),
  188. 'helpers' => ATOMIK_APP_ROOT . '/helpers',
  189. 'includes' => array(ATOMIK_APP_ROOT . '/includes', ATOMIK_APP_ROOT . '/libraries', ATOMIK_APP_ROOT . '/libs'),
  190. 'namespaces' => array(),
  191. 'overrides' => ATOMIK_APP_ROOT . '/overrides'
  192. ),
  193. /* @var array */
  194. 'files' => array(
  195. 'index' => 'index.php',
  196. 'config' => ATOMIK_APP_ROOT . '/config', // without extension
  197. 'bootstrap' => ATOMIK_APP_ROOT . '/bootstrap.php',
  198. 'pre_dispatch' => ATOMIK_APP_ROOT . '/pre_dispatch.php',
  199. 'post_dispatch' => ATOMIK_APP_ROOT . '/post_dispatch.php',
  200. '404' => ATOMIK_APP_ROOT . '/404.php',
  201. 'error' => ATOMIK_APP_ROOT . '/error.php',
  202. 'log' => ATOMIK_APP_ROOT . '/log.txt'
  203. ),
  204. /* @var bool */
  205. 'catch_errors' => false,
  206. /* @var bool */
  207. 'throw_errors' => false,
  208. /* @var array */
  209. 'error_report_attrs' => array(
  210. 'atomik-error' => 'style="padding: 10px"',
  211. 'atomik-error-title' => 'style="font-size: 1.3em; font-weight: bold; color: #FF0000"',
  212. 'atomik-error-lines' => 'style="width: 100%; margin-bottom: 20px; background-color: #fff;'
  213. . 'border: 1px solid #000; font-size: 0.8em"',
  214. 'atomik-error-line' => '',
  215. 'atomik-error-line-error' => 'style="background-color: #ffe8e7"',
  216. 'atomik-error-line-number' => 'style="background-color: #eeeeee"',
  217. 'atomik-error-line-text' => '',
  218. 'atomik-error-stack' => ''
  219. )
  220. ),
  221. /* @var int */
  222. 'start_time' => time() + microtime()
  223. ));
  224. /* -------------------------------------------------------------------------------------------
  225. * CORE
  226. * ------------------------------------------------------------------------------------------ */
  227. // creates the A function (shortcut to Atomik::get)
  228. if (!function_exists('A')) {
  229. /**
  230. * Shortcut function to Atomik::get()
  231. * Useful when dealing with selectors
  232. *
  233. * @see Atomik::get()
  234. * @return mixed
  235. */
  236. function A()
  237. {
  238. $args = func_get_args();
  239. return call_user_func_array(array('Atomik', 'get'), $args);
  240. }
  241. }
  242. // starts Atomik unless ATOMIK_AUTORUN is set to false
  243. if (!defined('ATOMIK_AUTORUN') || ATOMIK_AUTORUN === true) {
  244. Atomik::run();
  245. }
  246. /**
  247. * Exception class for Atomik
  248. *
  249. * @package Atomik
  250. */
  251. class Atomik_Exception extends Exception {}
  252. /**
  253. * HTTP Exception class for Atomik
  254. *
  255. * The code must be an HTTP response code
  256. *
  257. * @package Atomik
  258. */
  259. class Atomik_HttpException extends Atomik_Exception {}
  260. /**
  261. * Atomik Framework Main class
  262. *
  263. * @package Atomik
  264. */
  265. final class Atomik
  266. {
  267. /**
  268. * Global store
  269. *
  270. * This property is used to stored all data accessed using get(), set()...
  271. *
  272. * @var array
  273. */
  274. public static $store = array();
  275. /**
  276. * Atomik singleton
  277. *
  278. * @var Atomik
  279. */
  280. private static $instance;
  281. /**
  282. * Global store to reset to
  283. *
  284. * @var array
  285. */
  286. private static $reset = array();
  287. /**
  288. * Loaded plugins
  289. *
  290. * When a plugin is loaded, its name is saved in this array to
  291. * avoid loading it twice.
  292. *
  293. * @var array
  294. */
  295. private static $plugins = array();
  296. /**
  297. * Registered events
  298. *
  299. * The array keys are event names and their value is an array with
  300. * the event callbacks
  301. *
  302. * @var array
  303. */
  304. private static $events = array();
  305. /**
  306. * Selectors namespaces
  307. *
  308. * The array keys are the namespace name and the associated value is
  309. * the callback to call when the namespace is used
  310. *
  311. * @var array
  312. */
  313. private static $namespaces = array('flash' => array('Atomik', '_getFlashMessages'));
  314. /**
  315. * Execution contexts
  316. *
  317. * Each call to Atomik::execute() creates a context.
  318. *
  319. * @var array
  320. */
  321. private static $execContexts = array();
  322. /**
  323. * Pluggable applications
  324. *
  325. * @var array
  326. */
  327. private static $pluggableApplications = array();
  328. /**
  329. * Already loaded helpers
  330. *
  331. * @var array
  332. */
  333. private static $loadedHelpers = array();
  334. /**
  335. * Returns a singleton instance
  336. *
  337. * @return Atomik
  338. */
  339. public static function instance()
  340. {
  341. if (self::$instance === null) {
  342. self::$instance = new Atomik();
  343. }
  344. return self::$instance;
  345. }
  346. /**
  347. * Starts Atomik
  348. *
  349. * If dispatch is false, you will have to manually dispatch the request and exit.
  350. *
  351. * @param string $env A configuration key which will be merged at the root of the store
  352. * @param string $uri
  353. * @param bool $dispatch Whether to dispatch
  354. */
  355. public static function run($env = null, $uri = null, $dispatch = true)
  356. {
  357. // wrap the whole app inside a try/catch block to catch all errors
  358. try {
  359. @chdir(dirname(self::get('atomik/scriptname')));
  360. // config & environment
  361. self::loadConfig(self::get('atomik/files/config'), false);
  362. if ($env !== null && self::has($env)) {
  363. self::set(self::get($env));
  364. }
  365. self::fireEvent('Atomik::Config');
  366. // adds includes dirs to php include path
  367. $includePaths = array_merge(
  368. (array) self::get('atomik/dirs/includes', array()),
  369. array(get_include_path())
  370. );
  371. set_include_path(implode(PATH_SEPARATOR, $includePaths));
  372. // registers the error handler
  373. if (self::get('atomik/catch_errors', false) ||
  374. !self::get('atomik/throw_errors', false)) {
  375. set_error_handler('Atomik::_errorHandler');
  376. }
  377. // sets the error reporting to all errors if debug mode is on
  378. if (self::get('atomik/debug', false) == true) {
  379. error_reporting(E_ALL | E_STRICT);
  380. }
  381. // default logger
  382. if (self::get('atomik/log/register_default', false) == true) {
  383. self::listenEvent('Atomik::Log', 'Atomik::logToFile');
  384. }
  385. // registers the class autoload handler
  386. if (self::get('atomik/class_autoload', true) == true) {
  387. if (!function_exists('spl_autoload_register')) {
  388. throw new Atomik_Exception('Missing spl_autoload_register function');
  389. }
  390. spl_autoload_register('Atomik::autoload');
  391. }
  392. // cleans the plugins array
  393. $plugins = array();
  394. foreach (self::get('plugins', array()) as $key => $value) {
  395. if (!is_string($key)) {
  396. $key = $value;
  397. $value = array();
  398. }
  399. $plugins[ucfirst($key)] = (array) $value;
  400. }
  401. self::set('plugins', $plugins, false);
  402. // loads plugins
  403. // this method allows plugins that are being loaded to modify the plugins array
  404. $disabledPlugins = array();
  405. while (count($pluginsToLoad = array_diff(array_keys(self::get('plugins')),
  406. self::getLoadedPlugins(), $disabledPlugins)) > 0) {
  407. foreach ($pluginsToLoad as $plugin) {
  408. if (self::loadPlugin($plugin) === false) {
  409. $disabledPlugins[] = $plugin;
  410. }
  411. }
  412. }
  413. self::fireEvent('Atomik::Bootstrap');
  414. // loads bootstrap file
  415. if (file_exists($filename = self::get('atomik/files/bootstrap'))) {
  416. require($filename);
  417. }
  418. // starts the session
  419. if (self::get('atomik/start_session', true) == true) {
  420. session_start();
  421. if (($ns = self::get('atomik/session_namespace', false)) !== false) {
  422. if (!isset($_SESSION[$ns])) {
  423. $_SESSION[$ns] = array();
  424. }
  425. self::$store['session'] = &$_SESSION[$ns];
  426. } else {
  427. self::$store['session'] = &$_SESSION;
  428. }
  429. }
  430. // core is starting
  431. self::fireEvent('Atomik::Start', array(&$cancel));
  432. if ($cancel) {
  433. self::end(true);
  434. }
  435. self::log('Starting', LOG_DEBUG);
  436. // checks if url rewriting is used
  437. if (!self::has('atomik/url_rewriting')) {
  438. self::set('atomik/url_rewriting',
  439. isset($_SERVER['REDIRECT_URL']) || isset($_SERVER['REDIRECT_URI']));
  440. }
  441. // dispatches
  442. if ($dispatch) {
  443. self::dispatch($uri);
  444. self::end(true);
  445. }
  446. } catch (Exception $e) {
  447. self::log('[EXCEPTION: ' . $e->getCode() . '] ' . $e->getMessage(), LOG_ERR);
  448. self::fireEvent('Atomik::Error', array($e));
  449. // checks if we really want to catch errors
  450. if (self::get('atomik/catch_errors', false)) {
  451. self::renderException($e);
  452. } else if (self::get('atomik/throw_errors', false)) {
  453. throw $e;
  454. }
  455. header('Location: ', false, 500); // set the http response code
  456. self::end(false);
  457. }
  458. }
  459. /**
  460. * Loads a configuration file
  461. *
  462. * Supported format are php, ini and json
  463. * If the file's extension is not specified, the method will
  464. * search for a file with one of the supported extensions.
  465. *
  466. * @param string $filename
  467. * @param bool $triggerError Whether to throw an exception if the file does not exist
  468. */
  469. public static function loadConfig($filename, $triggerError = true)
  470. {
  471. self::fireEvent('Atomik::Loadconfig::Before', array(&$filename, &$triggerError));
  472. // config file format
  473. if (!preg_match('/.+\.(php|ini|json)$/', $filename)) {
  474. $found = false;
  475. foreach (array('php', 'ini', 'json') as $format) {
  476. if (file_exists($filename . '.' . $format)) {
  477. $found = true;
  478. break;
  479. }
  480. }
  481. if (!$found) {
  482. if ($triggerError) {
  483. throw new Atomik_Exception("Configuration file $filename not found");
  484. }
  485. return;
  486. }
  487. $filename .= '.' . $format;
  488. } else {
  489. $format = substr($filename, strrpos($filename, '.') + 1);
  490. }
  491. // loads the config file
  492. if ($format === 'php') {
  493. if (is_array($config = include($filename))) {
  494. self::set($config);
  495. }
  496. } else if ($format === 'ini') {
  497. if (($data = parse_ini_file($filename, true)) === false) {
  498. throw new Atomik_Exception('INI configuration malformed');
  499. }
  500. self::set(self::_dimensionizeArray($data, '.'), null, false);
  501. } else if ($format === 'json') {
  502. if (($config = json_decode(file_get_contents($filename), true)) === null) {
  503. throw new Atomik_Exception('JSON configuration malformed');
  504. }
  505. self::set($config);
  506. }
  507. self::fireEvent('Atomik::Loadconfig::After', array($filename, $triggerError));
  508. }
  509. /**
  510. * Dispatches the request
  511. *
  512. * It takes an URI, applies routes, executes the action and renders the view.
  513. * If $uri is null, the value of the GET parameter specified as the trigger
  514. * will be used.
  515. *
  516. * @param string $uri
  517. * @param bool $allowPluggableApplication Whether to allow plugin application to be loaded
  518. */
  519. public static function dispatch($uri = null, $allowPluggableApplication = true)
  520. {
  521. try {
  522. self::fireEvent('Atomik::Dispatch::Start', array(&$uri, &$allowPluggableApplication, &$cancel));
  523. if ($cancel) {
  524. return;
  525. }
  526. // checks if it's needed to auto discover the uri
  527. if ($uri === null) {
  528. // retreives the requested uri
  529. $trigger = self::get('atomik/trigger', 'action');
  530. if (isset($_GET[$trigger]) && !empty($_GET[$trigger])) {
  531. $uri = trim($_GET[$trigger], '/');
  532. }
  533. // retreives the base url
  534. if (self::get('atomik/base_url', null) === null) {
  535. if (self::get('atomik/url_rewriting') && (isset($_SERVER['REDIRECT_URL']) || isset($_SERVER['REDIRECT_URI']))) {
  536. // finds the base url from the redirected url
  537. $redirectUrl = isset($_SERVER['REDIRECT_URL']) ? $_SERVER['REDIRECT_URL'] : $_SERVER['REDIRECT_URI'];
  538. if (isset($_GET[$trigger])) {
  539. self::set('atomik/base_url', substr($redirectUrl, 0, -strlen($_GET[$trigger])));
  540. } else {
  541. self::set('atomik/base_url', $redirectUrl);
  542. }
  543. } else {
  544. // finds the base url from the script name
  545. self::set('atomik/base_url', rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\') . '/');
  546. }
  547. }
  548. } else {
  549. // sets the user defined request
  550. // retreives the base url
  551. if (self::get('atomik/base_url', null) === null) {
  552. // finds the base url from the script name
  553. self::set('atomik/base_url', rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\') . '/');
  554. }
  555. }
  556. // default uri
  557. if (empty($uri)) {
  558. $uri = self::get('app/default_action', 'index');
  559. }
  560. // routes the request
  561. $request = self::route($uri, $_GET);
  562. if (isset($request['@redirect'])) {
  563. self::redirect($request['@redirect']);
  564. }
  565. // checking if no dot are in the action name to avoid any hack attempt and if no
  566. // underscore is use as first character in a segment
  567. if (strpos($request['action'], '..') !== false || substr($request['action'], 0, 1) == '_'
  568. || strpos($request['action'], '/_') !== false) {
  569. throw new Atomik_Exception('Action outside of bound');
  570. }
  571. self::set('request_uri', $uri);
  572. self::set('request', $request);
  573. if (!self::has('full_request_uri')) {
  574. self::set('full_request_uri', $uri);
  575. }
  576. self::fireEvent('Atomik::Dispatch::Uri', array(&$uri, &$request, &$cancel));
  577. if ($cancel) {
  578. return;
  579. }
  580. // checks if the uri triggers a pluggable application
  581. if ($allowPluggableApplication) {
  582. foreach (self::$pluggableApplications as $plugin => $pluggAppConfig) {
  583. if (!self::uriMatch($pluggAppConfig['route'], $request['action'])) {
  584. continue;
  585. }
  586. // rewrite uri
  587. $baseAction = trim($pluggAppConfig['route'], '/*');
  588. $uri = substr(trim($request['action'], '/'), strlen($baseAction));
  589. if ($baseAction == '' && $uri == self::get('app/default_action')) {
  590. $uri = '';
  591. }
  592. self::set('atomik/base_action', $baseAction);
  593. // dispatches the pluggable application
  594. return self::dispatchPluggableApplication($plugin, $uri, $pluggAppConfig);
  595. }
  596. }
  597. // fetches the http method
  598. $httpMethod = $_SERVER['REQUEST_METHOD'];
  599. if (($param = self::get('app/http_method_param', false)) !== false) {
  600. // checks if the route parameter to override the method is defined
  601. $httpMethod = strtoupper(self::get($param, $httpMethod, $request));
  602. }
  603. if (!in_array($httpMethod, self::get('app/allowed_http_methods'))) {
  604. // specified method not allowed
  605. throw new Atomik_Exception('HTTP method not allowed');
  606. }
  607. self::set('app/http_method', strtoupper($httpMethod));
  608. // sets the view context
  609. self::setViewContext();
  610. // configuration is ok, ready to dispatch
  611. self::fireEvent('Atomik::Dispatch::Before', array(&$cancel));
  612. if ($cancel) {
  613. return;
  614. }
  615. self::log('Dispatching action ' . $request['action'], LOG_DEBUG);
  616. $vars = array();
  617. // pre dispatch action
  618. if (file_exists($filename = self::get('atomik/files/pre_dispatch'))) {
  619. list($content, $vars) = self::instance()->scoped($filename);
  620. }
  621. // executes the action
  622. ob_start();
  623. list($content, $vars) = self::execute(self::get('request/action'), true, $vars, true);
  624. $content = ob_get_clean() . $content;
  625. // whether to propagate vars to the layout or not
  626. if (!self::get('app/vars_to_layout', true)) {
  627. $vars = array();
  628. }
  629. // renders the layouts if enable
  630. if (($layout = self::get('app/layout', false)) !== false &&
  631. !self::get('app/disable_layout', false)) {
  632. $content = self::renderLayout($layout, $content, $vars);
  633. }
  634. // echoes the content
  635. self::fireEvent('Atomik::Output::Before', array(&$content));
  636. echo $content;
  637. self::fireEvent('Atomik::Output::After', array($content));
  638. // dispatch done
  639. self::fireEvent('Atomik::Dispatch::After');
  640. // post dispatch action
  641. if (file_exists($filename = self::get('atomik/files/post_dispatch'))) {
  642. require($filename);
  643. }
  644. } catch (Atomik_HttpException $e) {
  645. if ($e->getCode() == 404) {
  646. self::log('[404 NOT FOUND] ' . $e->getMessage(), LOG_ERR);
  647. self::fireEvent('Atomik::404', array($e));
  648. header('HTTP/1.0 404 Not Found');
  649. header('Content-type: text/html');
  650. if (file_exists($filename = self::get('atomik/files/404'))) {
  651. // includes the 404 error file
  652. include($filename);
  653. } else if (self::get('atomik/debug', false)) {
  654. echo '<h1>' . $e->getMessage() . '</h1>';
  655. } else {
  656. echo '<h1>Page not found</h1>';
  657. }
  658. self::end(false);
  659. }
  660. }
  661. }
  662. /**
  663. * Fires the Atomik::End event and exits the application
  664. *
  665. * @param bool $success Whether the application exit on success or because an error occured
  666. * @param bool $writeSession Whether to call session_write_close() before exiting
  667. */
  668. public static function end($success = false, $writeSession = true)
  669. {
  670. self::fireEvent('Atomik::End', array($success, &$writeSession));
  671. if ($writeSession) {
  672. session_write_close();
  673. }
  674. self::log('Ending', LOG_DEBUG);
  675. exit;
  676. }
  677. /**
  678. * Sets the view context
  679. *
  680. * View contexts are defined in app/views/contexts.
  681. * They can specify:
  682. * - an extension prefix (prefix)
  683. * - a layout (layout) (false disables the layout)
  684. * - an HTTP Content-Type (content_type)
  685. *
  686. * @param string $context
  687. */
  688. public static function setViewContext($context = null)
  689. {
  690. if ($context === null) {
  691. // fetches the view context
  692. $context = self::get(self::get('app/views/context_param', 'format'),
  693. self::get('app/views/default_context', 'html'),
  694. self::get('request'));
  695. }
  696. self::set('app/view_context', $context);
  697. // retreives view context params and prepare the response
  698. if (($viewContextParams = self::get('app/views/contexts/' . $context, false)) !== false) {
  699. if ($viewContextParams['layout'] !== true) {
  700. self::set('app/layout', $viewContextParams['layout']);
  701. }
  702. header('Content-type: ' .
  703. self::get('content_type', 'text/html', $viewContextParams));
  704. }
  705. }
  706. /**
  707. * Checks if an uri matches the pattern.
  708. *
  709. * The pattern can contain the * wildcard in any segment.
  710. * For example "users/*" will match all child actions of users.
  711. * If you want to match users and its children use "users*".
  712. *
  713. * Pattern is considered a regular expression if enclosed
  714. * between # (example: "#users/(.*)#")
  715. *
  716. * @param string $pattern
  717. * @param string $uri Default is the current request uri
  718. * @return bool
  719. */
  720. public static function uriMatch($pattern, $uri = null)
  721. {
  722. if ($uri === null) {
  723. $uri = self::get('request_uri');
  724. }
  725. $uri = trim($uri, '/');
  726. $pattern = trim($pattern, '/');
  727. if (self::get('atomik/urimatch_compat', false)) {
  728. // compatibility with 2.2
  729. if (substr($pattern, -2) == '/*') {
  730. $pattern = substr($pattern, 0, -2) . '*';
  731. }
  732. }
  733. $regexp = $pattern;
  734. if ($pattern{0} != '#') {
  735. $regexp = '#^' . str_replace('*', '(.*)', $pattern) . '$#';
  736. }
  737. return preg_match($regexp, $uri);
  738. }
  739. /**
  740. * Parses an uri to extract parameters
  741. *
  742. * Routes defines how to extract parameters from an uri. They can
  743. * have additional default parameters.
  744. * There are two kind of routes:
  745. *
  746. * - segments:
  747. * the uri is divided into path segments. Each segment can be
  748. * either static or a parameter (indicated by :).
  749. * eg: /archives/:year/:month
  750. *
  751. * - regexp:
  752. * uses a regexp against the uri. Must be enclosed using # instead of
  753. * slashes parameters must be specified as named subpattern.
  754. * eg: #^archives/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})$#
  755. *
  756. * If no route matches, the default route (ie :action) will automatically be used.
  757. * If the route ends with *, any additional segments will be added as parameters
  758. * eg: /archives/:year/* + /archives/2009/id/1 => year=2009 id=1
  759. *
  760. * You can also name your routes using the @name parameter (which won't be included
  761. * in the returned params). Named route can then be used with Atomik::url()
  762. *
  763. * @param string $uri
  764. * @param array $params Additional parameters which are not in the uri
  765. * @param array $routes Uses app/routes if null
  766. * @return array Route parameters
  767. */
  768. public static function route($uri, $params = array(), $routes = null)
  769. {
  770. if ($routes === null) {
  771. $routes = self::get('app/routes');
  772. }
  773. self::fireEvent('Atomik::Router::Start', array(&$uri, &$routes, &$params));
  774. // extracts uri information
  775. $components = parse_url($uri);
  776. $uri = trim($components['path'], '/');
  777. $uriSegments = explode('/', $uri);
  778. $uriExtension = false;
  779. if (isset($components['query'])) {
  780. parse_str($components['query'], $query);
  781. $params = array_merge($query, $params);
  782. }
  783. // extract the file extension from the uri
  784. $lastSegment = array_pop($uriSegments);
  785. if (($dot = strrpos($lastSegment, '.')) !== false) {
  786. $uriExtension = substr($lastSegment, $dot + 1);
  787. $lastSegment = substr($lastSegment, 0, $dot);
  788. }
  789. $uriSegments[] = $lastSegment;
  790. // checks if the extension must be present
  791. if (self::get('app/force_uri_extension', false) && $uriExtension === false) {
  792. throw new Atomik_Exception('Missing file extension');
  793. }
  794. // searches for a route matching the uri
  795. $found = false;
  796. $request = array();
  797. foreach (array_reverse($routes) as $route => $default) {
  798. if (!is_array($default)) {
  799. $default = array('action' => $default);
  800. }
  801. // removes the route name from the default params
  802. if (isset($default['@name'])) {
  803. unset($default['@name']);
  804. }
  805. // regexp
  806. if ($route{0} == '#') {
  807. if (!preg_match($route, $uri, $matches)) {
  808. continue;
  809. }
  810. unset($matches[0]);
  811. $found = true;
  812. $request = array_merge($default, $matches);
  813. break;
  814. }
  815. $segments = explode('/', trim($route, '/'));
  816. $request = $default;
  817. $extension = false;
  818. // extract the file extension from the route
  819. $lastSegment = array_pop($segments);
  820. if (($dot = strrpos($lastSegment, '.')) !== false) {
  821. $extension = substr($lastSegment, $dot + 1);
  822. $lastSegment = substr($lastSegment, 0, $dot);
  823. }
  824. // checks if additional params are allowed
  825. if (!($wildcard = $lastSegment == '*')) {
  826. $segments[] = $lastSegment;
  827. }
  828. // checks the extension
  829. if ($extension !== false) {
  830. if ($extension{0} == ':') {
  831. // extension is a parameter
  832. if ($uriExtension !== false) {
  833. $request[substr($extension, 1)] = $uriExtension;
  834. } else if (!isset($request[substr($extension, 1)])) {
  835. // no uri extension and no default value
  836. continue;
  837. }
  838. } else if ($extension != $uriExtension) {
  839. continue;
  840. }
  841. }
  842. for ($i = 0, $count = count($segments); $i < $count; $i++) {
  843. if (substr($segments[$i], 0, 1) == ':') {
  844. // segment is a parameter
  845. if (isset($uriSegments[$i])) {
  846. // this segment is defined in the uri
  847. $request[substr($segments[$i], 1)] = $uriSegments[$i];
  848. $segments[$i] = $uriSegments[$i];
  849. } else if (!array_key_exists(substr($segments[$i], 1), $default)) {
  850. // not defined in the uri and no default value
  851. continue 2;
  852. }
  853. } else {
  854. // fixed segment
  855. if (!isset($uriSegments[$i]) || $uriSegments[$i] != $segments[$i]) {
  856. continue 2;
  857. }
  858. }
  859. }
  860. // the "action" param must be set
  861. if (!isset($request['action']) && !isset($request['@redirect'])) {
  862. continue;
  863. }
  864. // if there's remaining segments in the uri
  865. if (($count = count($uriSegments)) > ($start = count($segments))) {
  866. if (!$wildcard || !self::get('atomik/auto_uri_wildcard', false)) {
  867. continue;
  868. }
  869. // adds them as params
  870. for ($i = $start; $i < $count; $i += 2) {
  871. if (isset($uriSegments[$i + 1])) {
  872. $request[$uriSegments[$i]] = $uriSegments[$i + 1];
  873. }
  874. }
  875. }
  876. $found = true;
  877. break;
  878. }
  879. if (!$found) {
  880. // route not found, creating default route
  881. $request = array(
  882. 'action' => implode('/', $uriSegments),
  883. self::get('app/views/context_param', 'format') => $uriExtension === false ?
  884. self::get('app/views/default_context', 'html') : $uriExtension
  885. );
  886. }
  887. $request = array_merge($params, $request);
  888. self::fireEvent('Atomik::Router::End', array($uri, &$request));
  889. return $request;
  890. }
  891. /**
  892. * Includes a file in the method scope and returns
  893. * public variables and the output buffer
  894. *
  895. * @internal
  896. * @param string $__filename Filename
  897. * @param array $__vars An array containing key/value pairs that will be transformed to variables accessible inside the file
  898. * @return array A tuple with the output buffer and the public variables
  899. */
  900. private function scoped($__filename, $__vars = array())
  901. {
  902. extract((array)$__vars);
  903. ob_start();
  904. include($__filename);
  905. $content = ob_get_clean();
  906. // retreives "public" variables (not prefixed with an underscore)
  907. $vars = array();
  908. foreach (get_defined_vars() as $name => $value) {
  909. if (substr($name, 0, 1) != '_') {
  910. $vars[$name] = $value;
  911. }
  912. }
  913. return array($content, $vars);
  914. }
  915. /* -------------------------------------------------------------------------------------------
  916. * Actions
  917. * ------------------------------------------------------------------------------------------ */
  918. /**
  919. * Executes an action using the executor specified in app/executor
  920. * Tries to execute the action. If this fail, it tries to render the view.
  921. * If neither of them are found, it will throw an exception.
  922. *
  923. * @see Atomik::render()
  924. * @param string $action The action name. The HTTP method can be suffixed after a dot
  925. * @param bool|string $viewContext The view context. Set to false to not render the view and return the variables or to true for the request's context
  926. * @return mixed The output of the view or an array of variables or false if an error occured
  927. */
  928. public static function execute($action, $viewContext = true, $vars = array(), $returnBoth = false)
  929. {
  930. $view = $action;
  931. $render = $viewContext !== false;
  932. $executor = self::get('app/executor', 'Atomik::executeFile');
  933. if (is_bool($viewContext)) {
  934. // using the request's context
  935. $viewContext = self::get('app/view_context');
  936. }
  937. // appends the context's prefix to the view name
  938. $prefix = self::get('app/views/contexts/' . $viewContext . '/prefix', $viewContext);
  939. if (!empty($prefix)) {
  940. $view .= '.' . $prefix;
  941. }
  942. // creates the execution context
  943. $context = array('action' => &$action, 'view' => &$view, 'vars' => &$vars,
  944. 'render' => &$render, 'executor' => &$executor);
  945. self::$execContexts[] =& $context;
  946. self::fireEvent('Atomik::Execute::Start', array(&$action, &$context, &$vars));
  947. if ($action === false) {
  948. self::trigger404('No action specified');
  949. }
  950. // checks if the method is specified in $action
  951. if (($dot = strrpos($action, '.')) !== false) {
  952. // it is, extract it
  953. $method = strtolower(substr($action, $dot + 1));
  954. $action = substr($action, 0, $dot);
  955. } else {
  956. // use the current request's http method
  957. $method = strtolower(self::get('app/http_method'));
  958. }
  959. $context['method'] = $method;
  960. self::fireEvent('Atomik::Execute::Before', array(&$action, &$context, &$vars));
  961. $vars = call_user_func($executor, $action, $method, $vars, $context);
  962. $viewFilename = self::viewFilename($view);
  963. if ($viewFilename === false) {
  964. if ($vars === false) {
  965. self::trigger404('No files found associated to the specified action');
  966. }
  967. // no view file, disabling view
  968. $view = false;
  969. }
  970. if ($vars === false) {
  971. $vars = array();
  972. }
  973. self::fireEvent('Atomik::Execute::After', array($action, &$context, &$vars));
  974. // deletes the execution context
  975. array_pop(self::$execContexts);
  976. // returns $vars if the view should not be rendered
  977. if ($render === false) {
  978. return $returnBoth ? array('', $vars) : $vars;
  979. }
  980. // no view
  981. if ($view === false) {
  982. return $returnBoth ? array('', $vars) : '';
  983. }
  984. // renders the view associated to the action
  985. $content = self::render($view, $vars);
  986. return $returnBoth ? array($content, $vars) : $content;
  987. }
  988. /**
  989. * Executor which uses files to define actions
  990. *
  991. * Searches for a file called after the action (with the php extension) inside
  992. * directories set under atomik/dirs/actions
  993. *
  994. * The content of this file can be anything.
  995. *
  996. * You can create an action file per http method by suffixing the action
  997. * name by the http method in lower case with a dot separating them.
  998. * (eg: submit action for POST => submit.post.php)
  999. * The non-http-method specific file (ie without any suffix) will always
  1000. * be executed before the http-method specific file and variables will
  1001. * be forwarded from one to another.
  1002. *
  1003. * @param string $action
  1004. * @param string $method
  1005. * @param array $context
  1006. * @return array
  1007. */
  1008. public static function executeFile($action, $method, $vars, $context)
  1009. {
  1010. // filenames
  1011. $methodAction = $action . '.' . $method;
  1012. $actionFilename = self::actionFilename($action);
  1013. $methodActionFilename = self::actionFilename($methodAction);
  1014. self::fireEvent('Atomik::Executefile', array(&$actionFilename, &$methodActionFilename, &$context));
  1015. // checks if at least one of the action files or the view file is defined
  1016. if ($actionFilename === false && $methodActionFilename === false) {
  1017. return false;
  1018. }
  1019. $atomik = self::instance();
  1020. // executes the global action
  1021. if ($actionFilename !== false) {
  1022. // executes the action in its own scope and fetches defined variables
  1023. list($content, $vars) = $atomik->scoped($actionFilename, $vars);
  1024. echo $content;
  1025. }
  1026. // executes the method specific action
  1027. if ($methodActionFilename !== false) {
  1028. // executes the action in its own scope and fetches defined variables
  1029. list($content, $vars) = $atomik->scoped($methodActionFilename, $vars);
  1030. echo $content;
  1031. }
  1032. return $vars;
  1033. }
  1034. /**
  1035. * Prevents the view of the actionfrom which it's called to be rendered
  1036. */
  1037. public static function noRender()
  1038. {
  1039. if (count(self::$execContexts)) {
  1040. self::$execContexts[count(self::$execContexts) - 1]['view'] = false;
  1041. }
  1042. }
  1043. /**
  1044. * Modifies the view associted to the action from which it's called
  1045. *
  1046. * @param string $view View name
  1047. */
  1048. public static function setView($view)
  1049. {
  1050. if (count(self::$execContexts)) {
  1051. self::$execContexts[count(self::$execContexts) - 1]['view'] = $view;
  1052. }
  1053. }
  1054. /**
  1055. * Disables the layout
  1056. *
  1057. * @param bool $disable Whether to disable the layout
  1058. */
  1059. public static function disableLayout($disable = true)
  1060. {
  1061. self::set('app/disable_layout', $disable);
  1062. }
  1063. /* -------------------------------------------------------------------------------------------
  1064. * Views
  1065. * ------------------------------------------------------------------------------------------ */
  1066. /**
  1067. * Renders a view
  1068. *
  1069. * Searches for a file called after the view inside
  1070. * directories configured in atomik/dirs/views. If no file is found, an
  1071. * exception is thrown unless $triggerError is false.
  1072. *
  1073. * @param string $view The view name
  1074. * @param array $vars An array containing key/value pairs that will be transformed to variables accessible inside the view
  1075. * @param array $dirs Directories where view files are stored
  1076. * @return string|bool
  1077. */
  1078. public static function render($view, $vars = array(), $dirs = null)
  1079. {
  1080. if ($dirs === null) {
  1081. $dirs = self::get('atomik/dirs/views');
  1082. }
  1083. self::fireEvent('Atomik::Render::Start', array(&$view, &$vars, &$dirs, &$triggerError));
  1084. // view filename
  1085. if (($filename = self::viewFilename($view, $dirs)) === false) {
  1086. self::trigger404('View ' . $view . ' not found');
  1087. }
  1088. self::fireEvent('Atomik::Render::Before', array(&$view, &$vars, &$filename, $triggerError));
  1089. $output = self::renderFile($filename, $vars);
  1090. self::fireEvent('Atomik::Render::After', array($view, &$output, $vars, $filename, $triggerError));
  1091. return $output;
  1092. }
  1093. /**
  1094. * Renders a file using a filename which will not be resolved.
  1095. *
  1096. * @param string $filename Filename
  1097. * @param array $vars An array containing key/value pairs that will be transformed to variables accessible inside the file
  1098. * @return string The output of the rendered file
  1099. */
  1100. public static function renderFile($filename, $vars = array())
  1101. {
  1102. self::fireEvent('Atomik::Renderfile::Before', array(&$filename, &$vars));
  1103. if (($callback = self::get('app/views/engine', false)) !== false) {
  1104. if (!is_callable($callback)) {
  1105. throw new Atomik_Exception('The specified rendering engine callback cannot be called');
  1106. }
  1107. $output = $callback($filename, $vars);
  1108. } else {
  1109. list($output, $vars) = self::instance()->scoped($filename, $vars);
  1110. }
  1111. self::fireEvent('Atomik::Renderfile::After', array($filename, &$output, $vars));
  1112. return $output;
  1113. }
  1114. /**
  1115. * Renders a layout
  1116. *
  1117. * @param string $layout Layout name
  1118. * @param string $content The content that will be available in the layout in the $contentForLayout variable
  1119. * @param array $vars An array containing key/value pairs that will be transformed to variables accessible inside the layout
  1120. * @param array $dirs Directories where to search for layouts
  1121. * @return string
  1122. */
  1123. public static function renderLayout($layout, $content, $vars = array(), $dirs = null)
  1124. {
  1125. if ($dirs === null) {
  1126. $dirs = self::get('atomik/dirs/layouts');
  1127. }
  1128. if (is_array($layout)) {
  1129. foreach (array_reverse($layout) as $lay) {
  1130. $content = self::renderLayout($lay, $content, $vars, $dirs);
  1131. }
  1132. return $content;
  1133. }
  1134. $appLayout = self::delete('app/layout');
  1135. self::set('app/layout', array($layout));
  1136. do {
  1137. $layout = array_shift(self::getRef('app/layout'));
  1138. self::fireEvent('Atomik::Renderlayout', array(&$layout, &$content, &$vars, &$dirs));
  1139. $vars['contentForLayout'] = $content;
  1140. $content = self::render($layout, $vars, $dirs);
  1141. } while (count(self::get('app/layout')));
  1142. self::set('app/layout', $appLayout);
  1143. return $content;
  1144. }
  1145. /* -------------------------------------------------------------------------------------------
  1146. * Helpers
  1147. * ------------------------------------------------------------------------------------------ */
  1148. /**
  1149. * Loads an helper file
  1150. *
  1151. * @param string $helperName
  1152. * @param array $dirs Directories where to search for helpers
  1153. */
  1154. public static function loadHelper($helperName, $dirs = null)
  1155. {
  1156. if (isset(self::$loadedHelpers[$helperName])) {
  1157. return;
  1158. }
  1159. if ($dirs === null) {
  1160. $dirs = self::get('atomik/dirs/helpers');
  1161. }
  1162. self::fireEvent('Atomik::Loadhelper::Before', array(&$helperName, &$dirs));
  1163. if (($filename = self::path($helperName . '.php', $dirs)) === false) {
  1164. throw new Atomik_Exception('Helper ' . $helperName . ' not found');
  1165. }
  1166. include $filename;
  1167. if (!function_exists($helperName)) {
  1168. // searching for an helper defined as a class
  1169. $camelizedHelperName = str_replace(' ', '', ucwords(str_replace('_', ' ', $helperName)));
  1170. $className = $camelizedHelperName . 'Helper';
  1171. if (!class_exists($className, false)) {
  1172. // neither a function nor a class has been found
  1173. throw new Exception('Helper ' . $helperName . ' file found but no function or class matching the helper name');
  1174. }
  1175. // helper defined as a class
  1176. self::$loadedHelpers[$helperName] = array(new $className(), $camelizedHelperName);
  1177. } else {
  1178. // helper defined as a function
  1179. self::$loadedHelpers[$helperName] = $helperName;
  1180. }
  1181. self::fireEvent('Atomik::Loadhelper::After', array($helperName, $dirs));
  1182. }
  1183. /**
  1184. * Registers an helper
  1185. *
  1186. * @param string $helperName
  1187. * @param callback $callback
  1188. */
  1189. public static function registerHelper($helperName, $callback)
  1190. {
  1191. self::$loadedHelpers[$helperName] = $callback;
  1192. }
  1193. /**
  1194. * Executes an helper
  1195. *
  1196. * @param string $helperName
  1197. * @param array $args Arguments for the helper
  1198. * @param array $dirs Directories where to search for helpers
  1199. * @return mixed
  1200. */
  1201. public static function helper($helperName, $args = array(), $dirs = null)
  1202. {
  1203. self::loadHelper($helperName, $dirs);
  1204. return call_user_func_array(self::$loadedHelpers[$helperName], $args);
  1205. }
  1206. /**
  1207. * PHP magic method to handle calls to helper in views
  1208. *
  1209. * @param string $helperName
  1210. * @param array $args
  1211. * @return mixed
  1212. */
  1213. public function __call($helperName, $args)
  1214. {
  1215. if (method_exists('Atomik', $helperName)) {
  1216. return call_user_func_array(array('Atomik', $helperName), $args);
  1217. }
  1218. return self::helper($helperName, $args);
  1219. }
  1220. /**
  1221. * PHP > 5.3 magic method to handle calls to undefined method
  1222. * Redirect calls to {@see Atomik::helper()}
  1223. *
  1224. * @param string $helperName
  1225. * @param array $args
  1226. * @return mixed
  1227. */
  1228. public static function __callStatic($helperName, $args)
  1229. {
  1230. return call_user_func(array(self::instance(), 'helper'), $helperName, $args);
  1231. }
  1232. /* -------------------------------------------------------------------------------------------
  1233. * Accessors
  1234. * ------------------------------------------------------------------------------------------ */
  1235. /**
  1236. * Sets a key/value pair in the store
  1237. *
  1238. * If the first argument is an array, values are merged recursively.
  1239. * The array is first dimensionized
  1240. * You can set values from sub arrays by using a path-like key.
  1241. * For example, to set the value inside the array $array[key1][key2]
  1242. * use the key 'key1/key2'
  1243. * Can be used on any array by specifying the third argument
  1244. *
  1245. * @see Atomik::_dimensionizeArray()
  1246. * @param array|string $key Can be an array to set many key/value
  1247. * @param mixed $value
  1248. * @param bool $dimensionize Whether to use Atomik::_dimensionizeArray() on $key
  1249. * @param array $array The array on which the operation is applied
  1250. * @param array $add Whether to add values or replace them
  1251. */
  1252. public static function set($key, $value = null, $dimensionize = true, &$array = null, $add = false)
  1253. {
  1254. // if $data is null, uses the global store
  1255. if ($array === null) {
  1256. $array = &self::$store;
  1257. }
  1258. // setting a key directly
  1259. if (is_string($key)) {
  1260. $parentArrayKey = strpos($key, '/') !== false ? dirname($key) : null;
  1261. $key = basename($key);
  1262. $parentArray = &self::getRef($parentArrayKey, $array);
  1263. if ($parentArray === null) {
  1264. $dimensionizedParentArray = self::_dimensionizeArray(array($parentArrayKey => null));
  1265. $array = self::_mergeRecursive($array, $dimensionizedParentArray);
  1266. $parentArray = &self::getRef($parentArrayKey, $array);
  1267. }
  1268. if ($add !== false) {
  1269. if (!isset($parentArray[$key]) || $parentArray[$key] === null) {
  1270. if (!is_array($value)) {
  1271. $parentArray[$key] = $value;
  1272. return;
  1273. }
  1274. $parentArray[$key] = array();
  1275. } else if (!is_array($parentArray[$key])) {
  1276. $parentArray[$key] = array($parentArray[$key]);
  1277. }
  1278. $value = is_array($value) ? $value : array($value);
  1279. if ($add == 'prepend') {
  1280. $parentArray[$key] = array_merge_recursive($value, $parentArray[$key]);
  1281. } else {
  1282. $parentArray[$key] = array_merge_recursive($parentArray[$key], $value);
  1283. }
  1284. } else {
  1285. $parentArray[$key] = $value;
  1286. }
  1287. return;
  1288. }
  1289. if (!is_array($key)) {
  1290. throw new Atomik_Exception('The first parameter of Atomik::set() must be a string or an array, ' . gettype($key) . ' given');
  1291. }
  1292. if ($dimensionize) {
  1293. $key = self::_dimensionizeArray($key);
  1294. }
  1295. // merges the store and the array
  1296. if ($add) {
  1297. $array = array_merge_recursive($array, $key);
  1298. } else {
  1299. $array = self::_mergeRecursive($array, $key);
  1300. }
  1301. }
  1302. /**
  1303. * Adds a value to the array pointed by the key
  1304. *
  1305. * If the first argument is an array, values are merged recursively.
  1306. * The array is first dimensionized
  1307. * You can add values to sub arrays by using a path-like key.
  1308. * For example, to add a value to the array $array[key1][key2]
  1309. * use the key 'key1/key2'
  1310. * If the value pointed by the key is not an array, it will be
  1311. * transformed to one.
  1312. * Can be used on any array by specifying the third argument
  1313. *
  1314. * @see Atomik::_dimensionizeArray()
  1315. * @param array|string $key Can be an array to add many key/value
  1316. * @param mixed $value
  1317. * @param bool $dimensionize Whether to use Atomik::_dimensionizeArray()
  1318. * @param array $array The array on which the operation is applied
  1319. */
  1320. public static function add($key, $value = null, $dimensionize = true, &$array = null)
  1321. {
  1322. return self::set($key, $value, $dimensionize, $array, 'append');
  1323. }
  1324. /**
  1325. * Prependes a value to the array pointed by the key
  1326. *
  1327. * Works the same as add()
  1328. *
  1329. * @see Atomik::add()
  1330. * @param array|string $key Can be an array to add many key/value
  1331. * @param mixed $value
  1332. * @param bool $dimensionize Whether to use Atomik::_dimensionizeArray()
  1333. * @param array $array The array on which the operation is applied
  1334. */
  1335. public static function prepend($key, $value = null, $dimensionize = true, &$array = null)
  1336. {
  1337. return self::set($key, $value, $dimensionize, $array, 'prepend');
  1338. }
  1339. /**
  1340. * Like array_merge() but recursively
  1341. *
  1342. * @internal
  1343. * @see array_merge()
  1344. * @param array $array1
  1345. * @param array $array2
  1346. * @return array
  1347. */
  1348. public static function _mergeRecursive($array1, $array2)
  1349. {
  1350. $array = $array1;
  1351. foreach ($array2 as $key => $value) {
  1352. if (is_array($value) && array_key_exists($key, $array1) && is_array($array1[$key])) {
  1353. $array[$key] = self::_mergeRecursive($array1[$key], $value);
  1354. continue;
  1355. }
  1356. $array[$key] = $value;
  1357. }
  1358. return $array;
  1359. }
  1360. /**
  1361. * Recursively checks array for path-like keys (ie. keys containing slashes)
  1362. * and transform them into multi dimensions array
  1363. *
  1364. * @internal
  1365. * @param array $array
  1366. * @param string $separator
  1367. * @return array
  1368. */
  1369. public static function _dimensionizeArray($array, $separator = '/')
  1370. {
  1371. $dimArray = array();
  1372. foreach ($array as $key => $value) {
  1373. // checks if the key is a path
  1374. if (strpos($key, $separator) !== false) {
  1375. $parts = explode($separator, $key);
  1376. $firstPart = array_shift($parts);
  1377. // recursively dimensionize the key
  1378. $value = self::_dimensionizeArray(array(implode($separator, $parts) => $value), $separator);
  1379. if (isset($dimArray[$firstPart])) {
  1380. if (!is_array($dimArray[$firstPart])) {
  1381. // if $firstPart exists but is not an array, drops the value and use an array
  1382. $dimArray[$firstPart] = array();
  1383. }
  1384. // merge recursively both arrays
  1385. $dimArray[$firstPart] = self::_mergeRecursive($dimArray[$firstPart], $value);
  1386. } else {
  1387. $dimArray[$firstPart] = $value;
  1388. }
  1389. } else if (is_array($value)) {
  1390. // dimensionize sub arrays
  1391. $value = self::_dimensionizeArray($value, $separator);
  1392. if (isset($dimArray[$key])) {
  1393. $dimArray[$key] = self::_mergeRecursive($dimArray[$key], $value);
  1394. } else {
  1395. $dimArray[$key] = $value;
  1396. }
  1397. } else {
  1398. $dimArray[$key] = $value;
  1399. }
  1400. }
  1401. return $dimArray;
  1402. }
  1403. /**
  1404. * Gets a value using its associatied key from the store
  1405. *
  1406. * You can fetch value from sub arrays by using a path-like
  1407. * key. Separate each key with a slash. For example if you want
  1408. * to fetch the value from an $store[key1][key2][key3] you can use
  1409. * key1/key2/key3
  1410. * Can be used on any array by specifying the third argument
  1411. *
  1412. * @param string|array $key The configuration key which value should be returned. If null, fetches all values
  1413. * @param mixed $default Default value if the key is not found
  1414. * @param array $array The array on which the operation is applied
  1415. * @return mixed
  1416. */
  1417. public static function get($key = null, $default = null, $array = null)
  1418. {
  1419. // checks if a namespace is used
  1420. if (is_string($key) && preg_match('/^([a-z]+):(.*)/', $key, $match)) {
  1421. // checks if the namespace exists */
  1422. if (isset(self::$namespaces[$match[1]])) {
  1423. // calls the namespace callback and returns
  1424. $args = func_get_args();
  1425. $args[0] = $match[2];
  1426. return call_user_func_array(self::$namespaces[$match[1]], $args);
  1427. }
  1428. }
  1429. if (($value = self::getRef($key, $array)) !== null) {
  1430. return $value;
  1431. }
  1432. // key not found, returns default
  1433. return $default;
  1434. }
  1435. /**
  1436. * Checks if a key is defined in the store
  1437. *
  1438. * Can check through sub array using a path-like key
  1439. * Can be used on any array by specifying the second argument
  1440. *
  1441. * @see Atomik::get()
  1442. * @param string $key The key which should be deleted
  1443. * @param array $array The array on which the operation is applied
  1444. * @return bool
  1445. */
  1446. public static function has($key, $array = null)
  1447. {
  1448. return self::getRef($key, $array) !== null;
  1449. }
  1450. /**
  1451. * Deletes a key from the store
  1452. *
  1453. * Can delete through sub array using a path-like key
  1454. * Can be used on any array by specifying the second argument
  1455. *
  1456. * @see Atomik::get()
  1457. * @param string $key
  1458. * @param array $array The array on which the operation is applied
  1459. * @return mixed The deleted value
  1460. */
  1461. public static function delete($key, &$array = null)
  1462. {
  1463. $parentArrayKey = strpos($key, '/') !== false ? dirname($key) : null;
  1464. $key = basename($key);
  1465. $parentArray = &self::getRef($parentArrayKey, $array);
  1466. if ($parentArray === null || !array_key_exists($key, $parentArray)) {
  1467. throw new Atomik_Exception('Key "' . $key . '" does not exists');
  1468. }
  1469. $value = $parentArray[$key];
  1470. unset($parentArray[$key]);
  1471. return $value;
  1472. }
  1473. /**
  1474. * Gets a reference to a value from the store using its associatied key
  1475. *
  1476. * You can fetch value from sub arrays by using a path-like
  1477. * key. Separate each key with a slash. For example if you want
  1478. * to fetch the value from an $store[key1][key2][key3] you can use
  1479. * key1/key2/key3
  1480. * Can be used on any array by specifying the second argument
  1481. *
  1482. * @param string|array $key The configuration key which value should be returned. If null, fetches all values
  1483. * @param array $array The array on which the operation is applied
  1484. * @return mixed Null if the key does not match
  1485. */
  1486. public static function &getRef($key = null, &$array = null)
  1487. {
  1488. $null = null;
  1489. // returns the store
  1490. if ($array === null) {
  1491. $array = &self::$store;
  1492. }
  1493. // return the whole arrat
  1494. if ($key === null) {
  1495. return $array;
  1496. }
  1497. // checks if the $key is an array
  1498. if (!is_array($key)) {
  1499. // checks if it has slashes
  1500. if (!strpos($key, '/')) {
  1501. if (array_key_exists($key, $array)) {
  1502. $value =& $array[$key];
  1503. return $value;
  1504. }
  1505. return $null;
  1506. }
  1507. // creates an array by spliting using slashes
  1508. $key = explode('/', $key);
  1509. }
  1510. // checks if the key exists
  1511. $firstKey = array_shift($key);
  1512. if (array_key_exists($firstKey, $array)) {
  1513. if (count($key) > 0) {
  1514. // there's still keys so it goes deeper
  1515. return self::getRef($key, $array[$firstKey]);
  1516. } else {
  1517. // the key has been found
  1518. $value =& $array[$firstKey];
  1519. return $value;
  1520. }
  1521. }
  1522. return $null;
  1523. }
  1524. /**
  1525. * Resets the global store
  1526. *
  1527. * If no argument are specified the store is resetted, otherwise value are set normally and the
  1528. * state is saved.
  1529. *
  1530. * @internal
  1531. * @see Atomik::set()
  1532. * @param array|string $key Can be an array to set many key/value
  1533. * @param mixed $value
  1534. * @param bool $dimensionize Whether to use Atomik::_dimensionizeArray() on $key
  1535. */
  1536. public static function reset($key = null, $value = null, $dimensionize = true)
  1537. {
  1538. if ($key !== null) {
  1539. self::set($key, $value, $dimensionize, self::$reset);
  1540. self::set($key, $value, $dimensionize);
  1541. return;
  1542. }
  1543. // reset
  1544. self::$store = self::_mergeRecursive(self::$store, self::$reset);
  1545. }
  1546. /**
  1547. * Registers a new selector namespace
  1548. *
  1549. * A namespace preceed a key. When used, $callback will be
  1550. * called instead of the normal logic. Applies only on get() calls.
  1551. *
  1552. * @param string $namespace
  1553. * @param callback $callback
  1554. */
  1555. public static function registerSelector($namespace, $callback)
  1556. {
  1557. self::$namespaces[$namespace] = $callback;
  1558. }
  1559. /* -------------------------------------------------------------------------------------------
  1560. * Plugins
  1561. * ------------------------------------------------------------------------------------------ */
  1562. /**
  1563. * Loads a plugin using the configuration specified under plugins
  1564. *
  1565. * @param string $name
  1566. * @return bool
  1567. */
  1568. public static function loadPlugin($name)
  1569. {
  1570. $name = ucfirst($name);
  1571. if (($config = &self::getRef('plugins/' . $name)) === null) {
  1572. $config = array();
  1573. }
  1574. return self::loadCustomPlugin($name, $config);
  1575. }
  1576. /**
  1577. * Loads a custom plugin
  1578. *
  1579. * Options:
  1580. * - dirs: Directories from where to load the plugin
  1581. * - classNameTemplate: % will be replaced with the plugin name
  1582. * - callStart: Whether to call the start() method on the plugin class
  1583. *
  1584. * @param string $plugin The plugin name
  1585. * @param array $config Configuration for this plugin
  1586. * @param array $options Options for loading this plugin
  1587. * @return bool Success
  1588. */
  1589. public static function loadCustomPlugin($plugin, &$config = array(), $options = array())
  1590. {
  1591. $plugin = ucfirst($plugin);
  1592. $defaultOptions = array(
  1593. 'dirs' => self::get('atomik/dirs/plugins'),
  1594. 'classNameTemplate' => '%Plugin',
  1595. 'callStart' => true
  1596. );
  1597. $options = array_merge($defaultOptions, $options);
  1598. // checks if the plugin is already loaded
  1599. if (self::isPluginLoaded($plugin)) {
  1600. return true;
  1601. }
  1602. self::fireEvent('Atomik::Plugin::Before', array(&$plugin, &$config, &$options));
  1603. // checks if $plugin has been set to false from one of the event callbacks
  1604. if ($plugin === false) {
  1605. return false;
  1606. }
  1607. // tries to load the plugin from a file
  1608. if (($filename = self::path($plugin . '.php', $options['dirs'])) === false) {
  1609. // no file, checks for a directory
  1610. if (($pluginDir = self::path($plugin, $options['dirs'])) === false) {
  1611. // plugin not found
  1612. throw new Atomik_Exception('Missing plugin (no file or directory matching plugin name): ' . $plugin);
  1613. }
  1614. // directory found, plugin file should be inside
  1615. $filename = $pluginDir . '/Plugin.php';
  1616. $appFilename = $pluginDir . '/Application.php';
  1617. if (!($isPluggApp = file_exists($appFilename)) && !file_exists($filename)) {
  1618. throw new Atomik_Exception('Missing plugin (no file inside the plugin\'s directory): ' . $plugin);
  1619. }
  1620. // registers the plugin as an application if Application.php exists
  1621. if ($isPluggApp && !isset(self::$pluggableApplications[$plugin])) {
  1622. self::registerPluggableApplication($plugin);
  1623. }
  1624. // adds the libraries folder from the plugin directory to the include path
  1625. if (@is_dir($pluginDir . '/libraries')) {
  1626. set_include_path($pluginDir . '/libraries'. PATH_SEPARATOR . get_include_path());
  1627. }
  1628. // adds the includes folder from the plugin directory to the include path
  1629. if (@is_dir($pluginDir . '/includes')) {
  1630. set_include_path($pluginDir . '/includes'. PATH_SEPARATOR . get_include_path());
  1631. }
  1632. } else {
  1633. $pluginDir = dirname($filename);
  1634. }
  1635. // loads the plugin
  1636. self::log('Loading plugin ' . $plugin, LOG_DEBUG);
  1637. self::instance()->scoped($filename, array('config' => $config));
  1638. // checks if the *Plugin class is defined. The use of this class
  1639. // is not mandatory in plugin file
  1640. $pluginClass = str_replace('%', $plugin, $options['classNameTemplate']);
  1641. if (class_exists($pluginClass, false)) {
  1642. $registerEventsCallback = true;
  1643. // call the start method on the plugin class if it's defined
  1644. if ($options['callStart'] && method_exists($pluginClass, 'start')) {
  1645. if (call_user_func(array($pluginClass, 'start'), &$config) === false) {
  1646. $registerEventsCallback = false;
  1647. }
  1648. }
  1649. // automatically registers events callback for methods starting with "on"
  1650. if ($registerEventsCallback) {
  1651. self::attachClassListeners($pluginClass);
  1652. }
  1653. }
  1654. self::fireEvent('Atomik::Plugin::After', array($plugin));
  1655. // stores the plugin name so we won't load it twice
  1656. // also stores the directory from where it was loaded
  1657. self::$plugins[$plugin] = isset($pluginDir) ? rtrim($pluginDir, DIRECTORY_SEPARATOR) : true;
  1658. return true;
  1659. }
  1660. /**
  1661. * Loads a plugin only if it's available
  1662. *
  1663. * @see Atomik::loadPlugin()
  1664. */
  1665. public static function loadPluginIfAvailable($plugin)
  1666. {
  1667. if (!self::isPluginLoaded($plugin) && self::isPluginAvailable($plugin)) {
  1668. self::loadPlugin($plugin);
  1669. }
  1670. }
  1671. /**
  1672. * Loads a custom plugin only if it's available
  1673. *
  1674. * @see Atomik::loadPlugin()
  1675. */
  1676. public static function loadCustomPluginIfAvailable($plugin, $config = array(), $options = array())
  1677. {
  1678. if (!self::isPluginLoaded($plugin) && self::isPluginAvailable($plugin)) {
  1679. self::loadCustomPlugin($plugin, $config, $options);
  1680. }
  1681. }
  1682. /**
  1683. * Checks if a plugin is already loaded
  1684. *
  1685. * @param string $plugin
  1686. * @return bool
  1687. */
  1688. public static function isPluginLoaded($plugin)
  1689. {
  1690. return isset(self::$plugins[ucfirst($plugin)]);
  1691. }
  1692. /**
  1693. * Checks if a plugin is available
  1694. *
  1695. * @param string $plugin
  1696. * @return bool
  1697. */
  1698. public static function isPluginAvailable($plugin)
  1699. {
  1700. if (self::path($plugin . '.php', self::get('atomik/dirs/plugins')) === false) {
  1701. return self::path($plugin, self::get('atomik/dirs/plugins')) !== false;
  1702. }
  1703. return true;
  1704. }
  1705. /**
  1706. * Returns all loaded plugins
  1707. *
  1708. * @param bool $withDir Whether to only returns plugin names or the name (as array key) and the directory
  1709. * @return array
  1710. */
  1711. public static function getLoadedPlugins($withDir = false)
  1712. {
  1713. if ($withDir) {
  1714. return self::$plugins;
  1715. }
  1716. return array_keys(self::$plugins);
  1717. }
  1718. /**
  1719. * Registers a pluggable application
  1720. *
  1721. * Possible configuration keys are:
  1722. * - rootDir: directory inside the plugin directory where the application is stored (default empty string)
  1723. * - pluginDir: the plugin's directory (default to null, will find the directory automatically)
  1724. * - resetConfig: whether to reset the config before dispatching
  1725. * - overwriteDirs: whether to keep access to the user actions, views, layouts and helpers folders
  1726. * - overwriteFiles: whether to keep access to the pre_dispatch and post_dispatch files
  1727. * - checkPluginIsLoaded: whether to check if the plugin is loaded
  1728. * - bootstrapFile: the name of the bootstrap file (eg: Application.php)
  1729. *
  1730. * @param string $plugin Plugin's name
  1731. * @param string $route The route that will trigger the application (default is the plugin name)
  1732. * @param array $config Configuration
  1733. */
  1734. public static function registerPluggableApplication($plugin, $route = null, $config = array())
  1735. {
  1736. $plugin = ucfirst($plugin);
  1737. self::fireEvent('Atomik::Registerpluggableapplication', array(&$plugin, &$route, &$config));
  1738. if (empty($plugin)) {
  1739. return;
  1740. }
  1741. // route
  1742. if ($route === null) {
  1743. // default route
  1744. $route = strtolower($plugin) . '*';
  1745. }
  1746. $config['route'] = $route;
  1747. self::$pluggableApplications[$plugin] = $config;
  1748. }
  1749. /**
  1750. * Dispatches a pluggable application
  1751. *
  1752. * @see Atomik::registerPluggableApplication()
  1753. * @param string $plugin Plugin's name
  1754. * @param string $uri Uri
  1755. * @param array $config Configuration
  1756. * @return bool Dispatch success
  1757. */
  1758. public static function dispatchPluggableApplication($plugin, $uri = null, $config = array())
  1759. {
  1760. $plugin = ucfirst($plugin);
  1761. // configuration
  1762. $defaultConfig = array(
  1763. 'rootDir' => '',
  1764. 'pluginDir' => null,
  1765. 'resetConfig' => true,
  1766. 'overwriteDirs' => true,
  1767. 'overwriteFiles' => true,
  1768. 'checkPluginIsLoaded' => true,
  1769. 'bootstrapFile' => 'Application.php'
  1770. );
  1771. $config = array_merge($defaultConfig, $config);
  1772. if ($config['checkPluginIsLoaded'] && !self::isPluginLoaded($plugin)) {
  1773. return false;
  1774. }
  1775. // params check
  1776. if (empty($uri)) {
  1777. $uri = '';
  1778. }
  1779. $rootDir = rtrim('/' . trim($config['rootDir'], '/'), '/');
  1780. // plugin dir
  1781. if ($config['pluginDir'] === null) {
  1782. $pluginDir = self::$plugins[$plugin];
  1783. } else {
  1784. $pluginDir = rtrim($config['pluginDir'], '/');
  1785. }
  1786. // application dir
  1787. $appDir = $pluginDir . $rootDir;
  1788. if (!is_dir($appDir)) {
  1789. throw new Atomik_Exception('To be used as an application, the plugin ' . $plugin . ' must use a directory');
  1790. }
  1791. // overrides dir
  1792. $overrideDir = self::path($plugin . $rootDir, self::get('atomik/dirs/overrides'));
  1793. if ($overrideDir === false) {
  1794. $overrideDir = ATOMIK_APP_ROOT . '/overrides/' . $plugin . $rootDir;
  1795. } else {
  1796. $overrideDir = rtrim($overrideDir, '/');
  1797. }
  1798. if (!self::has('app/running_plugin')) {
  1799. // saves user configuration
  1800. self::set('userapp', self::get('app'));
  1801. }
  1802. self::set('app/running_plugin', $plugin);
  1803. self::set('request_uri', $uri);
  1804. // resets the configuration but keep the layout
  1805. if ($config['resetConfig']) {
  1806. $layout = self::get('app/layout');
  1807. self::reset();
  1808. self::set('app/layout', $layout);
  1809. self::set('app/routes', array());
  1810. }
  1811. // rewrite dirs
  1812. $dirs = self::get('atomik/dirs');
  1813. $dirs['actions'] = array($overrideDir . '/actions', $appDir . '/actions');
  1814. $dirs['views'] = array($overrideDir . '/views', $appDir . '/views');
  1815. $overwritableDirs = array(
  1816. 'layouts' => array($overrideDir . '/layouts', $overrideDir . '/views',
  1817. $appDir . '/layouts', $appDir . '/views')
  1818. );
  1819. if ($config['overwriteDirs']) {
  1820. $dirs = array_merge($dirs, $overwritableDirs);
  1821. } else {
  1822. $dirs = array_merge_recursive($overwritableDirs, $dirs);
  1823. }
  1824. // do not overwrite helpers
  1825. $dirs['helpers'] = array_merge(
  1826. array($overrideDir . '/helpers', $appDir . '/helpers'),
  1827. $dirs['helpers']
  1828. );
  1829. self::set('atomik/dirs', $dirs);
  1830. // rewrite files
  1831. if ($config['overwriteFiles']) {
  1832. $files = self::get('atomik/files');
  1833. $files['pre_dispatch'] = $appDir . '/pre_dispatch.php';
  1834. $files['post_dispatch'] = $appDir . '/post_dispatch.php';
  1835. self::set('atomik/files', $files);
  1836. }
  1837. $cancel = false;
  1838. self::fireEvent('Atomik::Dispatchpluginapplication::Ready', array($plugin, &$uri, $config, &$cancel));
  1839. if ($cancel) {
  1840. return true;
  1841. }
  1842. // includes the bootstrap file
  1843. $bootstrapFile = $appDir . '/' . $config['bootstrapFile'];
  1844. if (file_exists($bootstrapFile)) {
  1845. $continue = include $bootstrapFile;
  1846. if ($continue === false) {
  1847. return true;
  1848. }
  1849. }
  1850. $cancel = false;
  1851. self::fireEvent('Atomik::Dispatchpluginapplication::Start', array($plugin, &$uri, $config, &$cancel));
  1852. if ($cancel) {
  1853. return true;
  1854. }
  1855. self::log('Dispatching pluggable application: ' . $plugin, LOG_DEBUG);
  1856. // re-dispatches the application
  1857. return self::dispatch($uri, false);
  1858. }
  1859. /* -------------------------------------------------------------------------------------------
  1860. * Events
  1861. * ------------------------------------------------------------------------------------------ */
  1862. /**
  1863. * Registers a callback to an event
  1864. *
  1865. * @param string $event Event name
  1866. * @param callback $callback The callback to call when the event is fired
  1867. * @param int $priority Listener priority
  1868. * @param bool $important If true and a listener of the same priority already exists, registers the new listener before the existing one.
  1869. */
  1870. public static function listenEvent($event, $callback, $priority = 50, $important = false)
  1871. {
  1872. // initialize the current event array */
  1873. if (!isset(self::$events[$event])) {
  1874. self::$events[$event] = array();
  1875. }
  1876. // while there is an event with the same priority, checks
  1877. // with an higher or lower priority
  1878. while (isset(self::$events[$event][$priority])) {
  1879. $priority += $important ? -1 : 1;
  1880. }
  1881. // stores the callback
  1882. self::$events[$event][$priority] = $callback;
  1883. }
  1884. /**
  1885. * Fires an event
  1886. *
  1887. * @param string $event The event name
  1888. * @param array $args Arguments for listeners
  1889. * @param bool $resultAsString Whether to return all listener results as a string
  1890. * @return array An array containing results of each executed listeners
  1891. */
  1892. public static function fireEvent($event, $args = array(), $resultAsString = false)
  1893. {
  1894. $results = array();
  1895. // executes all callback
  1896. if (isset(self::$events[$event])) {
  1897. $keys = array_keys(self::$events[$event]);
  1898. sort($keys);
  1899. foreach ($keys as $key) {
  1900. $callback = self::$events[$event][$key];
  1901. $results[$key] = call_user_func_array($callback, $args);
  1902. }
  1903. }
  1904. if ($resultAsString) {
  1905. return implode('', $results);
  1906. }
  1907. return $results;
  1908. }
  1909. /**
  1910. * Automatically registers event listeners for methods starting with "on"
  1911. *
  1912. * @param string|object $class
  1913. */
  1914. public static function attachClassListeners($class)
  1915. {
  1916. $methods = get_class_methods($class);
  1917. foreach ($methods as $method) {
  1918. if (preg_match('/^on[A-Z].*$/', $method)) {
  1919. $event = preg_replace('/(?<=\\w)([A-Z])/', '::\1', substr($method, 2));
  1920. self::listenEvent($event, array($class, $method));
  1921. }
  1922. }
  1923. }
  1924. /* -------------------------------------------------------------------------------------------
  1925. * Utilities
  1926. * ------------------------------------------------------------------------------------------ */
  1927. /**
  1928. * Builds a path.
  1929. *
  1930. * Multiple cases possible:
  1931. *
  1932. * 1) path($setOfPaths, $asArray = false)
  1933. * Returns a path from a set of paths (the set can be a string
  1934. * or an array). If the second argument is true, it returns
  1935. * all paths from the set as an array
  1936. *
  1937. * 2) path($file, $setOfPaths, $check = true)
  1938. * Searches for a file in the set of paths and returns the
  1939. * first one it finds. If $check is set to false, it returns
  1940. * the filename as if it was in the first path from the set of
  1941. * paths. Returns false when $check is true and no file where found.
  1942. *
  1943. * @param string|array $file
  1944. * @param string|array|bool $paths
  1945. * @param bool $check
  1946. * @return string|array
  1947. */
  1948. public static function path($file, $paths = null, $check = true)
  1949. {
  1950. // case1, $file is an array
  1951. if (is_array($file)) {
  1952. if ($paths === true) {
  1953. // returns $paths as array
  1954. return $file;
  1955. }
  1956. // returns the first path from the paths
  1957. return $file[0];
  1958. }
  1959. // $file is a string
  1960. // case 1
  1961. if ($paths === null || is_bool($paths)) {
  1962. if ($paths === true) {
  1963. // returns $file as array
  1964. return array($file);
  1965. }
  1966. // returns $file as string
  1967. return $file;
  1968. }
  1969. // case 2, $paths is an array or string
  1970. foreach (array_reverse((array) $paths) as $path) {
  1971. $filename = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $file;
  1972. if (!$check || file_exists($filename)) {
  1973. return $filename;
  1974. }
  1975. }
  1976. // nothing found
  1977. return false;
  1978. }
  1979. /**
  1980. * Returns an action's filename
  1981. *
  1982. * @see Atomik::path()
  1983. * @param string $action Action name
  1984. * @param array $dirs Directories where actions are stored (default is using configuration)
  1985. * @return string
  1986. */
  1987. public static function actionFilename($action, $dirs = null)
  1988. {
  1989. if ($dirs === null) {
  1990. $dirs = self::get('atomik/dirs/actions');
  1991. }
  1992. if (($filename = self::path($action . '.php', $dirs)) === false) {
  1993. return self::path($action . '/index.php', $dirs);
  1994. }
  1995. return $filename;
  1996. }
  1997. /**
  1998. * Returns a view's filename
  1999. *
  2000. * @see Atomik::path()
  2001. * @param string $view View name
  2002. * @param array $dirs Directories where views are stored (default is using configuration)
  2003. * @param string $extension View's file extension
  2004. * @return string
  2005. */
  2006. public static function viewFilename($view, $dirs = null, $extension = null)
  2007. {
  2008. if ($dirs === null) {
  2009. $dirs = self::get('atomik/dirs/views');
  2010. }
  2011. if ($extension === null) {
  2012. $extension = ltrim(self::get('app/views/file_extension'), '.');
  2013. }
  2014. if (($filename = self::path($view . '.' . $extension, $dirs)) === false) {
  2015. return self::path($view . '/index.' . $extension, $dirs);
  2016. }
  2017. return $filename;
  2018. }
  2019. /**
  2020. * Returns an url for the action depending on whether url rewriting
  2021. * is used or not. Will return an url relative to the current application scope.
  2022. * Ie. if a plugin uses this method, it will return an url for an action of itself
  2023. *
  2024. * Can be used on links starting with a protocol but they will of course
  2025. * not be resolved like action names.
  2026. * Named routes can also be used (only specify the route name - with the @ - as the action)
  2027. *
  2028. * The item "_merge_GET" can be use in $params to merge GET parameters with specified params.
  2029. *
  2030. * @param string $action The action name or an url. Can contain GET parameters (after ?)
  2031. * @param array $params GET parameters to be added to the query string, if true will reuse current GET params
  2032. * @param bool $useIndex Whether to use index.php in the url
  2033. * @param bool $useBaseAction Whether to prepend the action with atomik/base_action
  2034. * @return string
  2035. */
  2036. public static function url($action = null, $params = array(), $useIndex = true, $useBaseAction = true)
  2037. {
  2038. $trigger = self::get('atomik/trigger', 'action');
  2039. if ($params === false) {
  2040. $params = array();
  2041. }
  2042. if ($params === true || in_array('__merge_GET', $params)) {
  2043. if (!is_array($params)) {
  2044. $params = array();
  2045. }
  2046. $GET = $_GET;
  2047. if (isset($GET[$trigger])) {
  2048. unset($GET[$trigger]);
  2049. }
  2050. if (($i = array_search('__merge_GET', $params)) !== false) {
  2051. unset($params[$i]);
  2052. }
  2053. $params = self::_mergeRecursive($GET, $params);
  2054. }
  2055. if ($action === null) {
  2056. $action = self::get('request_uri');
  2057. }
  2058. // removes the query string from the action
  2059. if (($separator = strpos($action, '?')) !== false) {
  2060. $queryString = parse_url($action, PHP_URL_QUERY);
  2061. $action = substr($action, 0, $separator);
  2062. parse_str($queryString, $actionParams);
  2063. $params = self::_mergeRecursive($actionParams, $params);
  2064. }
  2065. // checks if it's a named route
  2066. if (strlen($action) > 0 && $action{0} == '@') {
  2067. $routeName = substr($action, 1);
  2068. $action = null;
  2069. foreach (self::get('app/routes') as $route => $default) {
  2070. if (!is_array($default) || !isset($default['@name']) ||
  2071. $default['@name'] != $routeName) {
  2072. continue;
  2073. }
  2074. if ($route{0} == '#') {
  2075. throw new Atomik_Exception('Named route (' . $routeName .
  2076. ') cannot use regular expressions');
  2077. }
  2078. $action = $route;
  2079. break;
  2080. }
  2081. if ($action === null) {
  2082. throw new Atomik_Exception('Missing route named ' . $routeName);
  2083. }
  2084. }
  2085. // injects parameters into the url
  2086. if (preg_match_all('/(:([a-zA-Z0-9_]+))/', $action, $matches)) {
  2087. for ($i = 0, $c = count($matches[0]); $i < $c; $i++) {
  2088. if (array_key_exists($matches[2][$i], $params)) {
  2089. $action = str_replace($matches[1][$i], $params[$matches[2][$i]], $action);
  2090. unset($params[$matches[2][$i]]);
  2091. }
  2092. }
  2093. }
  2094. // checks if $action is not a url (checking if there is a protocol)
  2095. if (!preg_match('/^([a-z]+):\/\/.*/', $action)) {
  2096. $action = ltrim($action, '/');
  2097. $url = rtrim(self::get('atomik/base_url', '.'), '/') . '/';
  2098. if ($useBaseAction) {
  2099. $action = ltrim(trim(self::get('atomik/base_action', ''), '/') . '/' . $action, '/');
  2100. }
  2101. // checks if url rewriting is used
  2102. if (!$useIndex || self::get('atomik/url_rewriting', false) == true) {
  2103. $useIndex = false;
  2104. $url .= $action;
  2105. } else {
  2106. // no url rewriting, using index.php
  2107. $url .= self::get('atomik/files/index', 'index.php');
  2108. $url .= '?' . $trigger . '=' . $action;
  2109. }
  2110. } else {
  2111. $useIndex = false;
  2112. $url = $action;
  2113. }
  2114. if (count($params)) {
  2115. $url .= ($useIndex ? '&amp;' : '?') . http_build_query($params, '', '&amp;');
  2116. }
  2117. // trigger an event
  2118. $args = func_get_args();
  2119. unset($args[0]);
  2120. self::fireEvent('Atomik::Url', array($action, &$url, $args));
  2121. return $url;
  2122. }
  2123. /**
  2124. * Returns an url exactly like Atomik::url() but relative to the root application.
  2125. *
  2126. * @see Atomik::url()
  2127. * @param string $action
  2128. * @param array $params
  2129. * @param bool $useIndex
  2130. * @return string
  2131. */
  2132. public static function appUrl($action = null, $params = array(), $useIndex = true)
  2133. {
  2134. return self::url($action, $params, $useIndex, false);
  2135. }
  2136. /**
  2137. * Returns an url exactly like Atomik::url() but relative to a plugin
  2138. *
  2139. * @see Atomik::url()
  2140. * @param string $plugin The name of a plugin which is used as a pluggable application
  2141. * @param string $action
  2142. * @param array $params
  2143. * @param bool $useIndex
  2144. * @return string
  2145. */
  2146. public static function pluginUrl($plugin, $action, $params = array(), $useIndex = true)
  2147. {
  2148. $plugin = ucfirst($plugin);
  2149. if (!isset(self::$pluggableApplications[$plugin])) {
  2150. throw new Atomik_Exception('Plugin ' . $plugin . ' is not registered as a pluggable application');
  2151. }
  2152. $route = rtrim(self::$pluggableApplications[$plugin]['route'], '/*');
  2153. return self::url($route . '/' . ltrim($action, '/'), $params, $useIndex, false);
  2154. }
  2155. /**
  2156. * Returns the url of an asset file (ie. an url without index.php) relative
  2157. * to the current application scope
  2158. *
  2159. * @see Atomik::url()
  2160. * @param string $filename
  2161. * @param array $params
  2162. * @return string
  2163. */
  2164. public static function asset($filename, $params = array())
  2165. {
  2166. if ($plugin = self::get('app/running_plugin')) {
  2167. return self::pluginAsset($plugin, $filename, $params);
  2168. }
  2169. return self::appAsset($filename, $params);
  2170. }
  2171. /**
  2172. * Returns the url of an asset file relative to the root application
  2173. *
  2174. * @see Atomik::asset()
  2175. * @param string $filename
  2176. * @param array $params
  2177. * @return string
  2178. */
  2179. public static function appAsset($filename, $params = array())
  2180. {
  2181. return self::url($filename, $params, false, false);
  2182. }
  2183. /**
  2184. * Returns the url of a plugin's asset file following the path template
  2185. * defined in the configuration.
  2186. *
  2187. * @see Atomik::url()
  2188. * @param string $plugin Plugin's name (default is the currently running pluggable app)
  2189. * @param string $filename
  2190. * @param array $params
  2191. * @return string
  2192. */
  2193. public static function pluginAsset($plugin, $filename, $params = array())
  2194. {
  2195. $template = self::get('atomik/plugin_assets_tpl', 'app/plugins/%s/assets');
  2196. $dirname = rtrim(sprintf($template, ucfirst($plugin)), '/');
  2197. $filename = '/' . ltrim($filename, '/');
  2198. return self::url($dirname . $filename, $params, false, false);
  2199. }
  2200. /*
  2201. * Includes a file
  2202. *
  2203. * @param string $include Filename or class name following the PEAR convention
  2204. * @param bool $className If true, $include will be transformed from a PEAR-formed class name to a filename
  2205. * @param string|array $dirs Include from specific directories rather than include path
  2206. * @return bool
  2207. */
  2208. public static function needed($include, $className = true, $dirs = null)
  2209. {
  2210. self::fireEvent('Atomik::Needed', array(&$include, &$className, &$dirs));
  2211. if ($include === null) {
  2212. return;
  2213. }
  2214. if ($dirs === null) {
  2215. $dirs = explode(PATH_SEPARATOR, get_include_path());
  2216. }
  2217. if ($className) {
  2218. $include = ltrim($include, '\\');
  2219. $namespaces = array_reverse(self::get('atomik/dirs/namespaces', array()));
  2220. foreach ($namespaces as $prefix => $dir) {
  2221. $prefix = ltrim($prefix, '\\');
  2222. if (substr($include, 0, strlen($prefix)) == $prefix) {
  2223. $include = substr($include, strlen($prefix));
  2224. $dirs = $dir;
  2225. break;
  2226. }
  2227. }
  2228. $include = str_replace(array('_', '\\'), DIRECTORY_SEPARATOR, $include);
  2229. }
  2230. $include .= '.php';
  2231. if (($filename = self::path($include, $dirs)) === false) {
  2232. return false;
  2233. }
  2234. return include_once($filename);
  2235. }
  2236. /**
  2237. * Class autoloader
  2238. *
  2239. * @see spl_autoload_register()
  2240. * @param string $className
  2241. * @return bool
  2242. */
  2243. public static function autoload($className)
  2244. {
  2245. try {
  2246. self::needed($className);
  2247. return true;
  2248. } catch (Exception $e) {
  2249. return false;
  2250. }
  2251. }
  2252. /**
  2253. * Escapes text so it can be outputted safely
  2254. *
  2255. * Uses escape profiles defined in the escaping configuration key
  2256. *
  2257. * @param string $text The text to escape
  2258. * @param mixed $profile A profile name, a function name, or an array of function
  2259. * @return string The escaped string
  2260. */
  2261. public static function escape($text, $profile = 'default')
  2262. {
  2263. if (!is_array($profile)) {
  2264. if (($functions = self::get('app/escaping/' . $profile, false)) === false) {
  2265. if (function_exists($profile)) {
  2266. $functions = array($profile);
  2267. } else {
  2268. $functions = array('htmlspecialchars');
  2269. }
  2270. }
  2271. } else {
  2272. $functions = $profile;
  2273. }
  2274. foreach ($functions as $function) {
  2275. $text = call_user_func($function, $text);
  2276. }
  2277. return $text;
  2278. }
  2279. /**
  2280. * Saves a message that can be retrieve only once
  2281. *
  2282. * @param string|array $message One message as a string or many messages as an array
  2283. * @param string $label
  2284. */
  2285. public static function flash($message, $label = 'default')
  2286. {
  2287. if (!isset($_SESSION)) {
  2288. throw new Atomik_Exception('The session must be started before using Atomik::flash()');
  2289. }
  2290. self::fireEvent('Atomik::Flash', array(&$message, &$label));
  2291. if (!self::has('session/__FLASH/' . $label)) {
  2292. self::set('session/__FLASH/' . $label, array());
  2293. }
  2294. self::add('session/__FLASH/' . $label, $message);
  2295. }
  2296. /**
  2297. * Returns the flash messages saved in the session
  2298. *
  2299. * @internal
  2300. * @param string $label Whether to only retreives messages from this label. When null or 'all', returns all messages
  2301. * @param bool $delete Whether to delete messages once retrieved
  2302. * @return array An array of messages if the label is specified or an array of array message
  2303. */
  2304. public static function _getFlashMessages($label = 'all', $delete = true) {
  2305. if (!self::has('session/__FLASH')) {
  2306. return array();
  2307. }
  2308. if (empty($label) || $label == 'all') {
  2309. if ($delete) {
  2310. return self::delete('session/__FLASH');
  2311. }
  2312. return self::get('session/__FLASH');
  2313. }
  2314. if (!self::has('session/__FLASH/' . $label)) {
  2315. return array();
  2316. }
  2317. if ($delete) {
  2318. return self::delete('session/__FLASH/' . $label);
  2319. }
  2320. return self::get('session/__FLASH/' . $label);
  2321. }
  2322. /**
  2323. * Filters data using PHP's filter extension
  2324. *
  2325. * @see filter_var()
  2326. * @param mixed $data
  2327. * @param mixed $filter
  2328. * @param mixed $options
  2329. * @param bool $falseOnFail
  2330. * @return mixed
  2331. */
  2332. public static function filter($data, $filter = null, $options = null, $falseOnFail = true)
  2333. {
  2334. if (is_array($data)) {
  2335. // the $filter must be a rule or a string to a rule defined under app/filters/rules
  2336. if (is_string($filter)) {
  2337. if (($rule = self::get('app/filters/rules/' . $filter, false)) === false) {
  2338. throw new Atomik_Exception('When $data is an array, the filter must be an array of definition or a rule name in Atomik::filter()');
  2339. }
  2340. } else {
  2341. $rule = $filter;
  2342. }
  2343. $results = array();
  2344. $messages = array();
  2345. $validate = true;
  2346. foreach ($rule as $field => $params) {
  2347. if (isset($data[$field]) && is_array($data[$field])) {
  2348. // data is an array
  2349. if (($results[$field] = self::filter($data[$field], $params)) === false) {
  2350. $messages[$field] = A('app/filters/messages', array());
  2351. $validate = false;
  2352. }
  2353. continue;
  2354. }
  2355. $filter = FILTER_SANITIZE_STRING;
  2356. $message = self::get('app/filters/default_message', 'The %s field failed to validate');
  2357. $required = false;
  2358. $default = null;
  2359. $label = $field;
  2360. if (is_array($params)) {
  2361. // extracting information from the array
  2362. if (isset($params['message'])) {
  2363. $message = self::delete('message', $params);
  2364. }
  2365. if (isset($params['required'])) {
  2366. $required = self::delete('required', $params);
  2367. }
  2368. if (isset($params['default'])) {
  2369. $default = self::delete('default', $params);
  2370. }
  2371. if (isset($params['label'])) {
  2372. $label = self::delete('label', $params);
  2373. }
  2374. if (isset($params['filter'])) {
  2375. $filter = self::delete('filter', $params);
  2376. }
  2377. $options = count($params) == 0 ? null : $params;
  2378. } else {
  2379. $filter = $params;
  2380. $options = null;
  2381. }
  2382. if (!isset($data[$field]) && !$required) {
  2383. // field not set and not required, do nothing
  2384. continue;
  2385. }
  2386. if ((!isset($data[$field]) || $data[$field] == '') && $required) {
  2387. // the field is required and either not set or empty, this is an error
  2388. $results[$field] = false;
  2389. $message = self::get('app/filters/required_message', 'The %s field must be filled');
  2390. } else if ($data[$field] === '' && !$required) {
  2391. // empty but not required, null value
  2392. $results[$field] = $default;
  2393. } else {
  2394. // normal, validating
  2395. $results[$field] = self::filter($data[$field], $filter, $options);
  2396. }
  2397. if ($results[$field] === false) {
  2398. // failed validation, adding the message
  2399. $messages[$field] = sprintf($message, $label);
  2400. $validate = false;
  2401. }
  2402. }
  2403. self::set('app/filters/messages', $messages);
  2404. return $validate || !$falseOnFail ? $results : false;
  2405. }
  2406. if (is_string($filter)) {
  2407. if (in_array($filter, filter_list())) {
  2408. // filter name from the extension filters
  2409. $filter = filter_id($filter);
  2410. } else if (preg_match('@/.+/[a-zA-Z]*@', $filter)) {
  2411. // regexp
  2412. $options = array('options' => array('regexp' => $filter));
  2413. $filter = FILTER_VALIDATE_REGEXP;
  2414. } else if (($callback = self::get('app/filters/callbacks/' . $filter, false)) !== false) {
  2415. // callback defined under app/filters/callbacks
  2416. $filter = FILTER_CALLBACK;
  2417. $options = $callback;
  2418. }
  2419. }
  2420. return filter_var($data, $filter, $options);
  2421. }
  2422. /**
  2423. * Makes a string friendly to urls
  2424. *
  2425. * @param string $string
  2426. * @return string
  2427. */
  2428. public static function friendlify($string)
  2429. {
  2430. $string = str_replace('-', ' ', $string);
  2431. $string = preg_replace(array('/\s+/', '/[^A-Za-z0-9\-]/'), array('-', ''), $string);
  2432. $string = trim(strtolower($string));
  2433. return $string;
  2434. }
  2435. /**
  2436. * Redirects to another url
  2437. *
  2438. * @see Atomik::url()
  2439. * @param string $url The url to redirect to
  2440. * @param bool $useUrl Whether to use Atomik::url() on $url before redirecting
  2441. * @param int $httpCode The redirection HTTP code
  2442. */
  2443. public static function redirect($url, $useUrl = true, $httpCode = 302)
  2444. {
  2445. self::fireEvent('Atomik::Redirect', array(&$url, &$useUrl, &$httpCode));
  2446. if ($url === false) {
  2447. return;
  2448. }
  2449. // uses Atomik::url()
  2450. if ($useUrl) {
  2451. $url = self::url($url);
  2452. }
  2453. if (isset($_SESSION)) {
  2454. $session = $_SESSION;
  2455. // seems to prevent a php bug with session before redirections
  2456. session_regenerate_id(true);
  2457. $_SESSION = $session;
  2458. // avoid loosing the session
  2459. session_write_close();
  2460. }
  2461. // redirects
  2462. header('Location: ' . $url, true, $httpCode);
  2463. self::end(true, false);
  2464. }
  2465. /**
  2466. * Same as Atomik::redirect() but for urls relative to the root application
  2467. *
  2468. * @see Atomik::redirect()
  2469. * @param string $url The url to redirect to
  2470. * @param bool $useUrl Use Atomik::url() on $url before redirecting
  2471. * @param int $httpCode The redirection HTTP code
  2472. */
  2473. public static function appRedirect($url, $useUrl = true, $httpCode = 302)
  2474. {
  2475. // uses Atomik::appUrl()
  2476. if ($useUrl) {
  2477. $url = self::appUrl($url);
  2478. }
  2479. self::redirect($url, false, $httpCode);
  2480. }
  2481. /**
  2482. * Same as Atomik::redirect() but redirects to a pluggable application
  2483. *
  2484. * @see Atomik::redirect()
  2485. * @param string $url The url to redirect to
  2486. * @param bool $useUrl Use Atomik::url() on $url before redirecting
  2487. * @param int $httpCode The redirection HTTP code
  2488. */
  2489. public static function pluginRedirect($plugin, $url, $useUrl = true, $httpCode = 302)
  2490. {
  2491. // uses Atomik::pluginUrl()
  2492. if ($useUrl) {
  2493. $url = self::pluginUrl($plugin, $url);
  2494. }
  2495. self::redirect($url, false, $httpCode);
  2496. }
  2497. /**
  2498. * Triggers a 404 error
  2499. *
  2500. * @param string $message
  2501. */
  2502. public static function trigger404($message = 'Not found')
  2503. {
  2504. throw new Atomik_HttpException($message, 404);
  2505. }
  2506. /**
  2507. * Fire an Atomik::Log event to which logger can listen
  2508. *
  2509. * @param string $message
  2510. * @param int $level
  2511. */
  2512. public static function log($message, $level = 3)
  2513. {
  2514. self::fireEvent('Atomik::Log', array($message, $level));
  2515. }
  2516. /**
  2517. * Default logger: log the message to the file defined in atomik/files/log
  2518. * The message template can be define in atomik/log/message_template
  2519. *
  2520. * @see Atomik::log()
  2521. * @param string $message
  2522. * @param int $level
  2523. */
  2524. public static function logToFile($message, $level)
  2525. {
  2526. if ($level > self::get('atomik/log/level')) {
  2527. return;
  2528. }
  2529. $filename = self::get('atomik/files/log');
  2530. $template = self::get('atomik/log/message_template', '[%date%] [%level%] %message%');
  2531. $tags = array(
  2532. '%date%' => @date('Y-m-d H:i:s'),
  2533. '%level%' => $level,
  2534. '%message%' => $message
  2535. );
  2536. $file = fopen($filename, 'a');
  2537. fwrite($file, str_replace(array_keys($tags), array_values($tags), $template) . "\n");
  2538. fclose($file);
  2539. $file = null;
  2540. }
  2541. /**
  2542. * Equivalent to var_dump() but can be disabled using the configuration
  2543. *
  2544. * @see var_dump()
  2545. * @param mixed $data The data which value should be dumped
  2546. * @param bool $force Always display the dump even if debug from the config is set to false
  2547. * @param bool $echo Whether to echo or return the result
  2548. * @return string The result or null if $echo is set to true
  2549. */
  2550. public static function debug($data, $force = false, $echo = true)
  2551. {
  2552. if (!$force && !self::get('atomik/debug', false)) {
  2553. return;
  2554. }
  2555. self::fireEvent('Atomik::Debug', array(&$data, &$force, &$echo));
  2556. // var_dump() does not support returns
  2557. ob_start();
  2558. var_dump($data);
  2559. $dump = ob_get_clean();
  2560. if (!$echo) {
  2561. return $dump;
  2562. }
  2563. echo $dump;
  2564. }
  2565. /**
  2566. * Catch errors and throw an ErrorException instead
  2567. *
  2568. * @internal
  2569. * @param int $errno
  2570. * @param string $errstr
  2571. * @param string $errfile
  2572. * @param int $errline
  2573. * @param mixed $errcontext
  2574. */
  2575. public static function _errorHandler($errno, $errstr, $errfile = '', $errline = 0, $errcontext = null)
  2576. {
  2577. // handles errors depending on the level defined with error_reporting
  2578. if (($errno & error_reporting()) == $errno) {
  2579. throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
  2580. }
  2581. }
  2582. /**
  2583. * Renders an exception
  2584. *
  2585. * @param Exception $exception The exception which sould ne rendered
  2586. * @param bool $return Return the output instead of printing it
  2587. * @return string
  2588. */
  2589. public static function renderException($exception, $return = false)
  2590. {
  2591. // checks if the user defined error file is available
  2592. if (file_exists($filename = self::get('atomik/files/error'))) {
  2593. include($filename);
  2594. self::end(false);
  2595. }
  2596. $attributes = self::get('atomik/error_report_attrs');
  2597. $html = '<div ' . $attributes['atomik-error'] . '>'
  2598. . '<span ' . $attributes['atomik-error-title'] . '>'
  2599. . $exception->getMessage() . '</span>'
  2600. . '<br />An error of type <strong>' . get_class($exception) . '</strong> '
  2601. . 'was caught at <strong>line ' . $exception->getLine() . '</strong><br />'
  2602. . 'in file <strong>' . $exception->getFile() . '</strong>'
  2603. . '<p>' . $exception->getMessage() . '</p>'
  2604. . '<table ' . $attributes['atomik-error-lines'] . '>';
  2605. // builds the table which display the lines around the error
  2606. $lines = file($exception->getFile());
  2607. $start = $exception->getLine() - 7;
  2608. $start = $start < 0 ? 0 : $start;
  2609. $end = $exception->getLine() + 7;
  2610. $end = $end > count($lines) ? count($lines) : $end;
  2611. for($i = $start; $i < $end; $i++) {
  2612. // color the line with the error. with standard Exception, lines are
  2613. if($i == $exception->getLine() - (get_class($exception) != 'ErrorException' ? 1 : 0)) {
  2614. $html .= '<tr ' . $attributes['atomik-error-line-error'] . '><td>';
  2615. }
  2616. else {
  2617. $html .= '<tr ' . $attributes['atomik-error-line'] . '>'
  2618. . '<td ' . $attributes['atomik-error-line-number'] . '>';
  2619. }
  2620. $html .= $i . '</td><td ' . $attributes['atomik-error-line-text'] . '>'
  2621. . (isset($lines[$i]) ? htmlspecialchars($lines[$i]) : '') . '</td></tr>';
  2622. }
  2623. $html .= '</table>'
  2624. . '<strong>Stack:</strong><p ' . $attributes['atomik-error-stack'] . '>'
  2625. . nl2br($exception->getTraceAsString())
  2626. . '</p></div>';
  2627. echo $html;
  2628. }
  2629. }