PageRenderTime 65ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/Atomik.php

http://atomikframework.googlecode.com/
PHP | 1804 lines | 903 code | 245 blank | 656 comment | 211 complexity | bf47181b5e0ae0d0daac558c9d0114ee 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;