PageRenderTime 79ms CodeModel.GetById 15ms app.highlight 43ms RepoModel.GetById 1ms app.codeStats 2ms

/Atomik.php

http://atomikframework.googlecode.com/
PHP | 3021 lines | 1630 code | 376 blank | 1015 comment | 356 complexity | 2f858217e550f90eb3ad1b742b9c91dc MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   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
  21define('ATOMIK_VERSION', '2.3');
  22!defined('ATOMIK_APP_ROOT') && define('ATOMIK_APP_ROOT', './app');
  23
  24
  25/* -------------------------------------------------------------------------------------------
  26 *  APPLICATION CONFIGURATION
  27 * ------------------------------------------------------------------------------------------ */
  28
  29Atomik::reset(array(
  30
  31    'app' => array(
  32    
  33        /* @var string */
  34        'default_action'        => 'index',
  35
  36        /* The name of the layout
  37         * Add multiple layouts using an array (will be rendered in reverse order)
  38         * @var array|bool|string */
  39        'layout'                => false,
  40    
  41        /* @var bool */
  42        'disable_layout'        => false,
  43    
  44        /* Whether to propagate view vars to the layout
  45         * @var bool */
  46        'vars_to_layout'        => true,
  47        
  48        /* An array where keys are route names and their value is an associative
  49         * array of default values
  50         * @see Atomik::route()
  51         * @var array */
  52        'routes'                => array(),
  53    
  54        /* @var bool */
  55        'force_uri_extension'   => false,
  56            
  57        /* List of escaping profiles where keys are profile names and their
  58         * value an array of callbacks
  59         * @see Atomik::escape()
  60         * @var array */
  61        'escaping' => array(
  62            'default'           => array('htmlspecialchars', 'nl2br')
  63        ),
  64    
  65        /* @see Atomik::filter()
  66         * @var array */
  67        'filters' => array(
  68        
  69            /* @var array */
  70            'rules'             => array(),
  71            
  72            /* @var array */
  73            'callbacks'         => array(),
  74            
  75            /* @var string */
  76            'default_message'   => 'The %s field failed to validate',
  77            
  78            /* @var string */
  79            'required_message'  => 'The %s field must be filled'
  80        ),
  81        
  82        /**
  83         * The callback used to execute actions
  84         * @var callback */
  85        'executor'              => array('Atomik', 'executeFile'),
  86    
  87        /* @see Atomik::render()
  88         * @var array */
  89        'views' => array(
  90        
  91            /* @var string */
  92            'file_extension'     => '.phtml',
  93            
  94            /* Alternative rendering engine
  95             * @see Atomik::renderFile()
  96             * @var callback */
  97            'engine'             => false,
  98            
  99            /* @var string */
 100            'default_context'    => 'html',
 101            
 102            /* The GET parameter to retrieve the current context
 103             * @var string */
 104            'context_param'      => 'format',
 105            
 106            /* List of contexts where keys are the context name.
 107             * Contexts can specify:
 108             *  - prefix (string): the view filename's extension prefix
 109             *  - layout (bool): whether the layout should be rendered
 110             *  - content_type (string): the HTTP response content type
 111             * @var array */
 112            'contexts' => array(
 113                'html' => array(
 114                    'prefix'         => '',
 115                    'layout'         => true,
 116                    'content_type'   => 'text/html'
 117                ),
 118                'ajax' => array(
 119                    'prefix'         => '',
 120                    'layout'         => false,
 121                    'content_type'   => 'text/html'
 122                ),
 123                'xml' => array(
 124                    'prefix'         => 'xml',
 125                    'layout'         => false,
 126                    'content_type'   => 'text/xml'
 127                ),
 128                'json' => array(
 129                    'prefix'         => 'json',
 130                    'layout'         => false,
 131                    'content_type'   => 'application/json'
 132                ),
 133                'js' => array(
 134                    'prefix'         => 'js',
 135                    'layout'         => false,
 136                    'content_type'   => 'text/javascript'
 137                ),
 138                'css' => array(
 139                    'prefix'         => 'css',
 140                    'layout'         => false,
 141                    'content_type'   => 'text/css'
 142                )
 143            )
 144        ),
 145        
 146        /* A parameter in the route that will allow to specify the http method 
 147         * (override the request's method). False to disable
 148         * @var string */
 149        'http_method_param'       => '_method',
 150        
 151        /* @var array */
 152        'allowed_http_methods'    => array('GET', 'POST', 'PUT', 'DELETE', 'TRACE', 'HEAD', 'OPTIONS', 'CONNECT')
 153        
 154     )       
 155));
 156
 157
 158/* -------------------------------------------------------------------------------------------
 159 *  CORE CONFIGURATION
 160 * ------------------------------------------------------------------------------------------ */
 161
 162Atomik::set(array(
 163
 164    /* @var array */
 165    'plugins'                    => array(),
 166
 167    /* @var array */
 168    'atomik' => array(
 169
 170        /* Atomik's filename
 171         * @var string */
 172        'scriptname'			 => __FILE__,
 173    
 174        /* Base url, set to null for auto detection
 175         * @var string */
 176        'base_url'               => null,
 177        
 178        /* Whether url rewriting is activated on the server
 179         * @var bool */
 180        'url_rewriting'          => false,
 181
 182        /* Keep compatibility with 2.2
 183         * @var bool */
 184        'urimatch_compat'		 => false,
 185
 186        /* Whether to automatically allow additional params at the end of routed uris
 187         * @var bool */
 188        'auto_uri_wildcard'		 => false,
 189    
 190        /* @var bool */
 191        'debug'                  => false,
 192    
 193        /* The GET parameter used to retreive the action
 194         * @var string */
 195        'trigger'                => 'action',
 196
 197        /* Whether to register the class autoloader
 198         * @var bool */
 199        'class_autoload'         => true,
 200        
 201        /* @var bool */
 202        'start_session'          => true,
 203
 204        /* @var string */
 205        'session_namespace'		=> false,
 206        
 207        /* Plugin's assets path template. 
 208         * %s will be replaced by the plugin's name
 209         * @see Atomik::pluginAsset()
 210         * @var string */
 211        'plugin_assets_tpl'      => 'app/plugins/%s/assets/',
 212
 213        /* @var array */
 214        'log' => array(
 215        
 216            /* @var bool */
 217            'register_default'   => false,
 218            
 219            /* From which level to start logging messages
 220             * @var int */
 221            'level'              => LOG_WARNING,
 222            
 223            /* Message template for the default logger
 224             * @see Atomik::logToFile()
 225             * @var string */
 226            'message_template'   => '[%date%] [%level%] %message%'
 227        ),
 228    
 229        /* @var array */
 230        'dirs' => array(
 231            'app'                => ATOMIK_APP_ROOT,
 232            'plugins'            => ATOMIK_APP_ROOT . '/plugins',
 233            'actions'            => ATOMIK_APP_ROOT . '/actions',
 234            'views'              => ATOMIK_APP_ROOT . '/views',
 235            'layouts'            => array(ATOMIK_APP_ROOT . '/views', ATOMIK_APP_ROOT . '/layouts'),
 236            'helpers'            => ATOMIK_APP_ROOT . '/helpers',
 237            'includes'           => array(ATOMIK_APP_ROOT . '/includes', ATOMIK_APP_ROOT . '/libraries', ATOMIK_APP_ROOT . '/libs'),
 238            'namespaces' 		 => array(),
 239            'overrides'          => ATOMIK_APP_ROOT . '/overrides'
 240        ),
 241    
 242        /* @var array */
 243        'files' => array(
 244            'index'              => 'index.php',
 245            'config'             => ATOMIK_APP_ROOT . '/config', // without extension
 246            'bootstrap'          => ATOMIK_APP_ROOT . '/bootstrap.php',
 247            'pre_dispatch'       => ATOMIK_APP_ROOT . '/pre_dispatch.php',
 248            'post_dispatch'      => ATOMIK_APP_ROOT . '/post_dispatch.php',
 249            '404'                => ATOMIK_APP_ROOT . '/404.php',
 250            'error'              => ATOMIK_APP_ROOT . '/error.php',
 251            'log'                => ATOMIK_APP_ROOT . '/log.txt'
 252        ),
 253        
 254        /* @var bool */
 255        'catch_errors'           => false,
 256        
 257        /* @var bool */
 258        'throw_errors'           => false,
 259        
 260        /* @var array */
 261        'error_report_attrs'     => array(
 262            'atomik-error'               => 'style="padding: 10px"',
 263            'atomik-error-title'         => 'style="font-size: 1.3em; font-weight: bold; color: #FF0000"',
 264            'atomik-error-lines'         => 'style="width: 100%; margin-bottom: 20px; background-color: #fff;'
 265                                          . 'border: 1px solid #000; font-size: 0.8em"',
 266            'atomik-error-line'          => '',
 267            'atomik-error-line-error'    => 'style="background-color: #ffe8e7"',
 268            'atomik-error-line-number'   => 'style="background-color: #eeeeee"',
 269            'atomik-error-line-text'     => '',
 270            'atomik-error-stack'         => ''
 271        )
 272        
 273    ),
 274    
 275    /* @var int */
 276    'start_time' => time() + microtime()
 277));
 278
 279
 280/* -------------------------------------------------------------------------------------------
 281 *  CORE
 282 * ------------------------------------------------------------------------------------------ */
 283
 284// creates the A function (shortcut to Atomik::get)
 285if (!function_exists('A')) {
 286    /**
 287     * Shortcut function to Atomik::get()
 288     * Useful when dealing with selectors
 289     *
 290     * @see Atomik::get()
 291     * @return mixed
 292     */
 293    function A()
 294    {
 295        $args = func_get_args();
 296        return call_user_func_array(array('Atomik', 'get'), $args);
 297    }
 298}
 299
 300// starts Atomik unless ATOMIK_AUTORUN is set to false
 301if (!defined('ATOMIK_AUTORUN') || ATOMIK_AUTORUN === true) {
 302    Atomik::run();
 303}
 304
 305/**
 306 * Exception class for Atomik
 307 * 
 308 * @package Atomik
 309 */
 310class Atomik_Exception extends Exception {}
 311
 312/**
 313 * HTTP Exception class for Atomik
 314 * 
 315 * The code must be an HTTP response code
 316 * 
 317 * @package Atomik
 318 */
 319class Atomik_HttpException extends Atomik_Exception {}
 320
 321/**
 322 * Atomik Framework Main class
 323 *
 324 * @package Atomik
 325 */
 326final class Atomik
 327{
 328    /**
 329     * Global store
 330     * 
 331     * This property is used to stored all data accessed using get(), set()...
 332     *
 333     * @var array
 334     */
 335    public static $store = array();
 336    
 337    /**
 338     * Atomik singleton
 339     *
 340     * @var Atomik
 341     */
 342    private static $instance;
 343    
 344    /**
 345     * Global store to reset to
 346     * 
 347     * @var array
 348     */
 349    private static $reset = array();
 350    
 351    /**
 352     * Loaded plugins
 353     * 
 354     * When a plugin is loaded, its name is saved in this array to 
 355     * avoid loading it twice.
 356     *
 357     * @var array
 358     */
 359    private static $plugins = array();
 360    
 361    /**
 362     * Registered events
 363     * 
 364     * The array keys are event names and their value is an array with 
 365     * the event callbacks
 366     *
 367     * @var array
 368     */
 369    private static $events = array();
 370    
 371    /**
 372     * Selectors namespaces
 373     * 
 374     * The array keys are the namespace name and the associated value is
 375     * the callback to call when the namespace is used
 376     *
 377     * @var array
 378     */
 379    private static $namespaces = array('flash' => array('Atomik', '_getFlashMessages'));
 380    
 381    /**
 382     * Execution contexts
 383     * 
 384     * Each call to Atomik::execute() creates a context.
 385     * 
 386     * @var array
 387     */
 388    private static $execContexts = array();
 389    
 390    /**
 391     * Pluggable applications
 392     * 
 393     * @var array
 394     */
 395    private static $pluggableApplications = array();
 396    
 397    /**
 398     * Already loaded helpers
 399     * 
 400     * @var array
 401     */
 402    private static $loadedHelpers = array();
 403    
 404    /**
 405     * Returns a singleton instance
 406     *
 407     * @return Atomik
 408     */
 409    public static function instance()
 410    {
 411        if (self::$instance === null) {
 412            self::$instance = new Atomik();
 413        }
 414        return self::$instance;
 415    }
 416    
 417    /**
 418     * Starts Atomik
 419     * 
 420     * If dispatch is false, you will have to manually dispatch the request and exit.
 421     * 
 422     * @param string $env A configuration key which will be merged at the root of the store
 423     * @param string $uri
 424     * @param bool $dispatch Whether to dispatch
 425     */
 426    public static function run($env = null, $uri = null, $dispatch = true)
 427    {
 428        // wrap the whole app inside a try/catch block to catch all errors
 429        try {
 430            @chdir(dirname(self::get('atomik/scriptname')));
 431             
 432            // config & environment
 433            self::loadConfig(self::get('atomik/files/config'), false);
 434            if ($env !== null && self::has($env)) {
 435                self::set(self::get($env));
 436            }
 437            
 438            self::fireEvent('Atomik::Config');
 439            
 440            // adds includes dirs to php include path
 441            $includePaths = array_merge(
 442                (array) self::get('atomik/dirs/includes', array()),
 443                array(get_include_path())
 444            );
 445            set_include_path(implode(PATH_SEPARATOR, $includePaths));
 446            
 447            // registers the error handler
 448            if (self::get('atomik/catch_errors', false) || 
 449                !self::get('atomik/throw_errors', false)) {
 450                    set_error_handler('Atomik::_errorHandler');
 451            }
 452            
 453            // sets the error reporting to all errors if debug mode is on
 454            if (self::get('atomik/debug', false) == true) {
 455                error_reporting(E_ALL | E_STRICT);
 456            }
 457            
 458            // default logger
 459            if (self::get('atomik/log/register_default', false) == true) {
 460                self::listenEvent('Atomik::Log', 'Atomik::logToFile');
 461            }
 462            
 463            // registers the class autoload handler
 464            if (self::get('atomik/class_autoload', true) == true) {
 465                if (!function_exists('spl_autoload_register')) {
 466                    throw new Atomik_Exception('Missing spl_autoload_register function');
 467                }
 468                spl_autoload_register('Atomik::autoload');
 469            }
 470        
 471            // cleans the plugins array
 472            $plugins = array();
 473            foreach (self::get('plugins', array()) as $key => $value) {
 474                if (!is_string($key)) {
 475                    $key = $value;
 476                    $value = array();
 477                }
 478                $plugins[ucfirst($key)] = (array) $value;
 479            }
 480            self::set('plugins', $plugins, false);
 481            
 482            // loads plugins
 483            // this method allows plugins that are being loaded to modify the plugins array
 484            $disabledPlugins = array();
 485            while (count($pluginsToLoad = array_diff(array_keys(self::get('plugins')), 
 486                self::getLoadedPlugins(), $disabledPlugins)) > 0) {
 487                    foreach ($pluginsToLoad as $plugin) {
 488                        if (self::loadPlugin($plugin) === false) {
 489                            $disabledPlugins[] = $plugin;
 490                        }
 491                    }
 492            }
 493            
 494            self::fireEvent('Atomik::Bootstrap');
 495             
 496            // loads bootstrap file
 497            if (file_exists($filename = self::get('atomik/files/bootstrap'))) {
 498                require($filename);
 499            }
 500            
 501            // starts the session
 502            if (self::get('atomik/start_session', true) == true) {
 503                session_start();
 504                if (($ns = self::get('atomik/session_namespace', false)) !== false) {
 505                    if (!isset($_SESSION[$ns])) {
 506                        $_SESSION[$ns] = array();
 507                    }
 508                    self::$store['session'] = &$_SESSION[$ns];
 509                } else {
 510                    self::$store['session'] = &$_SESSION;
 511                }
 512            }
 513        
 514            // core is starting
 515            self::fireEvent('Atomik::Start', array(&$cancel));
 516            if ($cancel) {
 517                self::end(true);
 518            }
 519            self::log('Starting', LOG_DEBUG);
 520        
 521            // checks if url rewriting is used
 522            if (!self::has('atomik/url_rewriting')) {
 523                self::set('atomik/url_rewriting', 
 524                    isset($_SERVER['REDIRECT_URL']) || isset($_SERVER['REDIRECT_URI']));
 525            }
 526            
 527            // dispatches
 528            if ($dispatch) {
 529                self::dispatch($uri);
 530                self::end(true);
 531            }
 532            
 533        } catch (Exception $e) {
 534            self::log('[EXCEPTION: ' . $e->getCode() . '] ' . $e->getMessage(), LOG_ERR);
 535            self::fireEvent('Atomik::Error', array($e));
 536            
 537            // checks if we really want to catch errors
 538            if (self::get('atomik/catch_errors', false)) {
 539                self::renderException($e);
 540            } else if (self::get('atomik/throw_errors', false)) {
 541                throw $e;
 542            }
 543            
 544            header('Location: ', false, 500); // set the http response code
 545            self::end(false);
 546        }
 547    }
 548    
 549    /**
 550     * Loads a configuration file
 551     *
 552     * Supported format are php, ini and json
 553     * If the file's extension is not specified, the method will
 554     * search for a file with one of the supported extensions.
 555     *
 556     * @param string $filename
 557     * @param bool $triggerError Whether to throw an exception if the file does not exist
 558     */
 559    public static function loadConfig($filename, $triggerError = true)
 560    {
 561        self::fireEvent('Atomik::Loadconfig::Before', array(&$filename, &$triggerError));
 562        
 563        // config file format
 564        if (!preg_match('/.+\.(php|ini|json)$/', $filename)) {
 565            $found = false;
 566            foreach (array('php', 'ini', 'json') as $format) {
 567                if (file_exists($filename . '.' . $format)) {
 568                    $found = true;
 569                    break;
 570                }
 571            }
 572            if (!$found) {
 573                if ($triggerError) {
 574                    throw new Atomik_Exception("Configuration file $filename not found");
 575                }
 576                return;
 577            }
 578            $filename .= '.' . $format;
 579        } else {
 580            $format = substr($filename, strrpos($filename, '.') + 1);
 581        }
 582        
 583        // loads the config file
 584        if ($format === 'php') {
 585            if (is_array($config = include($filename))) {
 586                self::set($config);
 587            }
 588        } else if ($format === 'ini') {
 589            if (($data = parse_ini_file($filename, true)) === false) {
 590                throw new Atomik_Exception('INI configuration malformed');
 591            }
 592            self::set(self::_dimensionizeArray($data, '.'), null, false);
 593        } else if ($format === 'json') {
 594            if (($config = json_decode(file_get_contents($filename), true)) === null) {
 595                throw new Atomik_Exception('JSON configuration malformed');
 596            }
 597            self::set($config);
 598        }
 599        
 600        self::fireEvent('Atomik::Loadconfig::After', array($filename, $triggerError));
 601    }
 602    
 603    /**
 604     * Dispatches the request
 605     * 
 606     * It takes an URI, applies routes, executes the action and renders the view.
 607     * If $uri is null, the value of the GET parameter specified as the trigger 
 608     * will be used.
 609     * 
 610     * @param string $uri
 611     * @param bool $allowPluggableApplication Whether to allow plugin application to be loaded
 612     */
 613    public static function dispatch($uri = null, $allowPluggableApplication = true)
 614    {
 615        try {
 616            self::fireEvent('Atomik::Dispatch::Start', array(&$uri, &$allowPluggableApplication, &$cancel));
 617            if ($cancel) {
 618                return;
 619            }
 620            
 621            // checks if it's needed to auto discover the uri
 622            if ($uri === null) {
 623                
 624                // retreives the requested uri
 625                $trigger = self::get('atomik/trigger', 'action');
 626                if (isset($_GET[$trigger]) && !empty($_GET[$trigger])) {
 627                    $uri = trim($_GET[$trigger], '/');
 628                }
 629        
 630                // retreives the base url
 631                if (self::get('atomik/base_url', null) === null) {
 632                    if (self::get('atomik/url_rewriting') && (isset($_SERVER['REDIRECT_URL']) || isset($_SERVER['REDIRECT_URI']))) {
 633                        // finds the base url from the redirected url
 634                        $redirectUrl = isset($_SERVER['REDIRECT_URL']) ? $_SERVER['REDIRECT_URL'] : $_SERVER['REDIRECT_URI'];
 635                        if (isset($_GET[$trigger])) {
 636                            self::set('atomik/base_url', substr($redirectUrl, 0, -strlen($_GET[$trigger])));
 637                        } else {
 638                            self::set('atomik/base_url', $redirectUrl);
 639                        }
 640                    } else {
 641                        // finds the base url from the script name
 642                        self::set('atomik/base_url', rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\') . '/');
 643                    }
 644                }
 645                
 646            } else {
 647                // sets the user defined request
 648                // retreives the base url
 649                if (self::get('atomik/base_url', null) === null) {
 650                    // finds the base url from the script name
 651                    self::set('atomik/base_url', rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\') . '/');
 652                }
 653            }
 654            
 655            // default uri
 656            if (empty($uri)) {
 657                $uri = self::get('app/default_action', 'index');
 658            }
 659            
 660            // routes the request
 661            $request = self::route($uri, $_GET);
 662            if (isset($request['@redirect'])) {
 663                self::redirect($request['@redirect']);
 664            }
 665                
 666            // checking if no dot are in the action name to avoid any hack attempt and if no 
 667            // underscore is use as first character in a segment
 668            if (strpos($request['action'], '..') !== false || substr($request['action'], 0, 1) == '_' 
 669                || strpos($request['action'], '/_') !== false) {
 670                    throw new Atomik_Exception('Action outside of bound');
 671            }
 672            
 673            self::set('request_uri', $uri);
 674            self::set('request', $request);
 675            if (!self::has('full_request_uri')) {
 676                self::set('full_request_uri', $uri);
 677            }
 678            
 679            self::fireEvent('Atomik::Dispatch::Uri', array(&$uri, &$request, &$cancel));
 680            if ($cancel) {
 681                return;
 682            }
 683            
 684            // checks if the uri triggers a pluggable application
 685            if ($allowPluggableApplication) {
 686                foreach (self::$pluggableApplications as $plugin => $pluggAppConfig) {
 687                    if (!self::uriMatch($pluggAppConfig['route'], $request['action'])) {
 688                        continue;
 689                    }
 690                    
 691                    // rewrite uri
 692                    $baseAction = trim($pluggAppConfig['route'], '/*');
 693                    $uri = substr(trim($request['action'], '/'), strlen($baseAction));
 694                    if ($baseAction == '' && $uri == self::get('app/default_action')) {
 695                        $uri = '';
 696                    }
 697                    self::set('atomik/base_action', $baseAction);
 698                    
 699                    // dispatches the pluggable application
 700                    return self::dispatchPluggableApplication($plugin, $uri, $pluggAppConfig);
 701                }
 702            }
 703            
 704            // fetches the http method
 705            $httpMethod = $_SERVER['REQUEST_METHOD'];
 706            if (($param = self::get('app/http_method_param', false)) !== false) {
 707                // checks if the route parameter to override the method is defined
 708                $httpMethod = strtoupper(self::get($param, $httpMethod, $request));
 709            }
 710            if (!in_array($httpMethod, self::get('app/allowed_http_methods'))) {
 711                // specified method not allowed
 712                throw new Atomik_Exception('HTTP method not allowed');
 713            }
 714            self::set('app/http_method', strtoupper($httpMethod));
 715            
 716            // sets the view context
 717            self::setViewContext();
 718        
 719            // configuration is ok, ready to dispatch
 720            self::fireEvent('Atomik::Dispatch::Before', array(&$cancel));
 721            if ($cancel) {
 722                return;
 723            }
 724            
 725            self::log('Dispatching action ' . $request['action'], LOG_DEBUG);
 726            $vars = array();
 727        
 728            // pre dispatch action
 729            if (file_exists($filename = self::get('atomik/files/pre_dispatch'))) {
 730                list($content, $vars) = self::instance()->scoped($filename);
 731            }
 732            
 733            // executes the action
 734            ob_start();
 735            list($content, $vars) = self::execute(self::get('request/action'), true, $vars, true);
 736            $content = ob_get_clean() . $content;
 737            
 738            // whether to propagate vars to the layout or not
 739            if (!self::get('app/vars_to_layout', true)) {
 740                $vars = array();
 741            }
 742            
 743            // renders the layouts if enable
 744            if (($layout = self::get('app/layout', false)) !== false && 
 745                !self::get('app/disable_layout', false)) {
 746                    $content = self::renderLayout($layout, $content, $vars);
 747            }
 748            
 749            // echoes the content
 750            self::fireEvent('Atomik::Output::Before', array(&$content));
 751            echo $content;
 752            self::fireEvent('Atomik::Output::After', array($content));
 753        
 754            // dispatch done
 755            self::fireEvent('Atomik::Dispatch::After');
 756        
 757            // post dispatch action
 758            if (file_exists($filename = self::get('atomik/files/post_dispatch'))) {
 759                require($filename);
 760            }
 761            
 762        } catch (Atomik_HttpException $e) {
 763            if ($e->getCode() == 404) {
 764                self::log('[404 NOT FOUND] ' . $e->getMessage(), LOG_ERR);
 765                self::fireEvent('Atomik::404', array($e));
 766                        
 767                header('HTTP/1.0 404 Not Found');
 768                header('Content-type: text/html');
 769                
 770                if (file_exists($filename = self::get('atomik/files/404'))) {
 771                    // includes the 404 error file
 772                    include($filename);
 773                } else if (self::get('atomik/debug', false)) {
 774                    echo '<h1>' . $e->getMessage() . '</h1>';
 775                } else {
 776                    echo '<h1>Page not found</h1>';
 777                }
 778                
 779                self::end(false);
 780            }
 781        }
 782    }
 783
 784    /**
 785     * Fires the Atomik::End event and exits the application
 786     *
 787     * @param bool $success Whether the application exit on success or because an error occured
 788     * @param bool $writeSession Whether to call session_write_close() before exiting
 789     */
 790    public static function end($success = false, $writeSession = true)
 791    {
 792        self::fireEvent('Atomik::End', array($success, &$writeSession));
 793        
 794        if ($writeSession) {
 795            session_write_close();
 796        }
 797        
 798        self::log('Ending', LOG_DEBUG);
 799        exit;
 800    }
 801    
 802    /**
 803     * Sets the view context
 804     * 
 805     * View contexts are defined in app/views/contexts. 
 806     * They can specify:
 807     * 	- an extension prefix (prefix)
 808     *  - a layout (layout) (false disables the layout)
 809     *  - an HTTP Content-Type (content_type)
 810     * 
 811     * @param string $context
 812     */
 813    public static function setViewContext($context = null)
 814    {
 815        if ($context === null) {
 816            // fetches the view context
 817            $context = self::get(self::get('app/views/context_param', 'format'), 
 818                                self::get('app/views/default_context', 'html'), 
 819                                self::get('request'));
 820        }
 821        
 822        self::set('app/view_context', $context);
 823        
 824        // retreives view context params and prepare the response
 825        if (($viewContextParams = self::get('app/views/contexts/' . $context, false)) !== false) {
 826            if ($viewContextParams['layout'] !== true) {
 827                self::set('app/layout', $viewContextParams['layout']);
 828            }
 829            header('Content-type: ' . 
 830                self::get('content_type', 'text/html', $viewContextParams));
 831        }
 832    }
 833    
 834    /**
 835     * Checks if an uri matches the pattern. 
 836     * 
 837     * The pattern can contain the * wildcard in any segment.
 838     * For example "users/*" will match all child actions of users.
 839     * If you want to match users and its children use "users*".
 840     * 
 841     * Pattern is considered a regular expression if enclosed
 842     * between # (example: "#users/(.*)#")
 843     * 
 844     * @param string $pattern
 845     * @param string $uri Default is the current request uri
 846     * @return bool
 847     */
 848    public static function uriMatch($pattern, $uri = null)
 849    {
 850        if ($uri === null) {
 851            $uri = self::get('request_uri');
 852        }
 853        $uri = trim($uri, '/');
 854        $pattern = trim($pattern, '/');
 855        
 856        if (self::get('atomik/urimatch_compat', false)) {
 857            // compatibility with 2.2
 858            if (substr($pattern, -2) == '/*') {
 859                $pattern = substr($pattern, 0, -2) . '*';
 860            }
 861        }
 862        
 863        $regexp = $pattern;
 864        if ($pattern{0} != '#') {
 865            $regexp = '#^' . str_replace('*', '(.*)', $pattern) . '$#';
 866        }
 867        
 868        return preg_match($regexp, $uri);
 869    }
 870    
 871    /**
 872     * Parses an uri to extract parameters
 873     * 
 874     * Routes defines how to extract parameters from an uri. They can
 875     * have additional default parameters.
 876     * There are two kind of routes:
 877     *
 878     *  - segments: 
 879     *    the uri is divided into path segments. Each segment can be
 880     *    either static or a parameter (indicated by :).
 881     *    eg: /archives/:year/:month
 882     *
 883     *  - regexp:
 884     *    uses a regexp against the uri. Must be enclosed using # instead of
 885     *    slashes parameters must be specified as named subpattern.
 886     *    eg: #^archives/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})$#
 887     *
 888     * If no route matches, the default route (ie :action) will automatically be used.
 889     * If the route ends with *, any additional segments will be added as parameters
 890     * eg: /archives/:year/* + /archives/2009/id/1 => year=2009 id=1
 891     * 
 892     * You can also name your routes using the @name parameter (which won't be included
 893     * in the returned params). Named route can then be used with Atomik::url()
 894     *
 895     * @param string $uri
 896     * @param array $params Additional parameters which are not in the uri
 897     * @param array $routes Uses app/routes if null
 898     * @return array Route parameters
 899     */
 900    public static function route($uri, $params = array(), $routes = null)
 901    {
 902        if ($routes === null) {
 903            $routes = self::get('app/routes');
 904        }
 905        
 906        self::fireEvent('Atomik::Router::Start', array(&$uri, &$routes, &$params));
 907        
 908        // extracts uri information
 909        $components = parse_url($uri);
 910        $uri = trim($components['path'], '/');
 911        $uriSegments = explode('/', $uri);
 912        $uriExtension = false;
 913        if (isset($components['query'])) {
 914            parse_str($components['query'], $query);
 915            $params = array_merge($query, $params);
 916        }
 917        
 918        // extract the file extension from the uri
 919        $lastSegment = array_pop($uriSegments);
 920        if (($dot = strrpos($lastSegment, '.')) !== false) {
 921            $uriExtension = substr($lastSegment, $dot + 1);
 922            $lastSegment = substr($lastSegment, 0, $dot);
 923        }
 924        $uriSegments[] = $lastSegment;
 925        
 926        // checks if the extension must be present
 927        if (self::get('app/force_uri_extension', false) && $uriExtension === false) {
 928            throw new Atomik_Exception('Missing file extension');
 929        }
 930        
 931        // searches for a route matching the uri
 932        $found = false;
 933        $request = array();
 934        foreach (array_reverse($routes) as $route => $default) {
 935            if (!is_array($default)) {
 936                $default = array('action' => $default);
 937            }
 938            
 939            // removes the route name from the default params
 940            if (isset($default['@name'])) {
 941            	unset($default['@name']);
 942            }
 943            
 944            // regexp
 945            if ($route{0} == '#') {
 946                if (!preg_match($route, $uri, $matches)) {
 947                    continue;
 948                }
 949                unset($matches[0]);
 950                $found = true;
 951                $request = array_merge($default, $matches);
 952                break;
 953            }
 954            
 955            $segments = explode('/', trim($route, '/'));
 956            $request = $default;
 957            $extension = false;
 958            
 959            // extract the file extension from the route
 960            $lastSegment = array_pop($segments);
 961            if (($dot = strrpos($lastSegment, '.')) !== false) {
 962                $extension = substr($lastSegment, $dot + 1);
 963                $lastSegment = substr($lastSegment, 0, $dot);
 964            }
 965            // checks if additional params are allowed
 966            if (!($wildcard = $lastSegment == '*')) {
 967                $segments[] = $lastSegment;
 968            }
 969            
 970            // checks the extension
 971            if ($extension !== false) {
 972                if ($extension{0} == ':') {
 973                    // extension is a parameter
 974                    if ($uriExtension !== false) {
 975                        $request[substr($extension, 1)] = $uriExtension;
 976                    } else if (!isset($request[substr($extension, 1)])) {
 977                        // no uri extension and no default value
 978                        continue;
 979                    }
 980                } else if ($extension != $uriExtension) {
 981                    continue;
 982                }
 983            }
 984            
 985            for ($i = 0, $count = count($segments); $i < $count; $i++) {
 986                if (substr($segments[$i], 0, 1) == ':') {
 987                    // segment is a parameter
 988                    if (isset($uriSegments[$i])) {
 989                        // this segment is defined in the uri
 990                        $request[substr($segments[$i], 1)] = $uriSegments[$i];
 991                        $segments[$i] = $uriSegments[$i];
 992                    } else if (!array_key_exists(substr($segments[$i], 1), $default)) {
 993                        // not defined in the uri and no default value
 994                        continue 2;
 995                    }
 996                } else {
 997                    // fixed segment
 998                    if (!isset($uriSegments[$i]) || $uriSegments[$i] != $segments[$i]) {
 999                        continue 2;
1000                    }
1001                }
1002            }
1003            
1004            // the "action" param must be set
1005            if (!isset($request['action']) && !isset($request['@redirect'])) {
1006                continue;
1007            }
1008            
1009            // if there's remaining segments in the uri
1010            if (($count = count($uriSegments)) > ($start = count($segments))) {
1011                if (!$wildcard || !self::get('atomik/auto_uri_wildcard', false)) {
1012                    continue;
1013                }
1014                // adds them as params
1015                for ($i = $start; $i < $count; $i += 2) {
1016                    if (isset($uriSegments[$i + 1])) {
1017                        $request[$uriSegments[$i]] = $uriSegments[$i + 1];
1018                    }
1019                }
1020            }
1021            
1022            $found = true;
1023            break;
1024        }
1025        
1026        if (!$found) {
1027            // route not found, creating default route
1028            $request = array(
1029                'action' => implode('/', $uriSegments), 
1030                self::get('app/views/context_param', 'format') => $uriExtension === false ? 
1031                    self::get('app/views/default_context', 'html') : $uriExtension
1032            );
1033        }
1034        
1035        $request = array_merge($params, $request);
1036        self::fireEvent('Atomik::Router::End', array($uri, &$request));
1037        
1038        return $request;
1039    }
1040    
1041    /**
1042     * Includes a file in the method scope and returns
1043     * public variables and the output buffer
1044     * 
1045     * @internal
1046     * @param string $__filename Filename
1047     * @param array $__vars An array containing key/value pairs that will be transformed to variables accessible inside the file
1048     * @return array A tuple with the output buffer and the public variables
1049     */
1050    private function scoped($__filename, $__vars = array())
1051    {
1052        extract((array)$__vars);
1053        ob_start();
1054        include($__filename);
1055        $content = ob_get_clean();
1056        
1057        // retreives "public" variables (not prefixed with an underscore)
1058        $vars = array();
1059        foreach (get_defined_vars() as $name => $value) {
1060            if (substr($name, 0, 1) != '_') {
1061                $vars[$name] = $value;
1062            }
1063        }
1064        
1065        return array($content, $vars);
1066    }
1067    
1068    
1069    /* -------------------------------------------------------------------------------------------
1070     *  Actions
1071     * ------------------------------------------------------------------------------------------ */
1072    
1073    /**
1074     * Executes an action using the executor specified in app/executor
1075     
1076     * Tries to execute the action. If this fail, it tries to render the view.
1077     * If neither of them are found, it will throw an exception.
1078     *
1079     * @see Atomik::render()
1080     * @param string $action The action name. The HTTP method can be suffixed after a dot
1081     * @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
1082     * @return mixed The output of the view or an array of variables or false if an error occured
1083     */
1084    public static function execute($action, $viewContext = true, $vars = array(), $returnBoth = false)
1085    {
1086        $view = $action;
1087        $render = $viewContext !== false;
1088        $executor = self::get('app/executor', 'Atomik::executeFile');
1089        
1090        if (is_bool($viewContext)) {
1091            // using the request's context
1092            $viewContext = self::get('app/view_context');
1093        }
1094        // appends the context's prefix to the view name
1095        $prefix = self::get('app/views/contexts/' . $viewContext . '/prefix', $viewContext);
1096        if (!empty($prefix)) {
1097            $view .= '.' . $prefix;
1098        }
1099        
1100        // creates the execution context
1101        $context = array('action' => &$action, 'view' => &$view, 'vars' => &$vars,
1102                            'render' => &$render, 'executor' => &$executor);
1103        self::$execContexts[] =& $context;
1104    
1105        self::fireEvent('Atomik::Execute::Start', array(&$action, &$context, &$vars));
1106        if ($action === false) {
1107            self::trigger404('No action specified');
1108        }
1109        
1110        // checks if the method is specified in $action
1111        if (($dot = strrpos($action, '.')) !== false) {
1112            // it is, extract it
1113            $method = strtolower(substr($action, $dot + 1));
1114            $action = substr($action, 0, $dot);
1115        } else {
1116            // use the current request's http method
1117            $method = strtolower(self::get('app/http_method'));
1118        }
1119        $context['method'] = $method;
1120    
1121        self::fireEvent('Atomik::Execute::Before', array(&$action, &$context, &$vars));
1122        
1123        $vars = call_user_func($executor, $action, $method, $vars, $context);
1124        
1125        $viewFilename = self::viewFilename($view);
1126        if ($viewFilename === false) {
1127            if ($vars === false) {
1128                self::trigger404('No files found associated to the specified action');
1129            }
1130            // no view file, disabling view
1131            $view = false;
1132        }
1133        
1134        if ($vars === false) {
1135            $vars = array();
1136        }
1137        
1138        self::fireEvent('Atomik::Execute::After', array($action, &$context, &$vars));
1139        
1140        // deletes the execution context
1141        array_pop(self::$execContexts);
1142        
1143        // returns $vars if the view should not be rendered
1144        if ($render === false) {
1145            return $returnBoth ? array('', $vars) : $vars;
1146        }
1147        // no view
1148        if ($view === false) {
1149            return $returnBoth ? array('', $vars) : '';
1150        }
1151        
1152        // renders the view associated to the action
1153        $content =  self::render($view, $vars);
1154        return $returnBoth ? array($content, $vars) : $content;
1155    }
1156    
1157    /**
1158     * Executor which uses files to define actions
1159     *
1160     * Searches for a file called after the action (with the php extension) inside
1161     * directories set under atomik/dirs/actions
1162     *
1163     * The content of this file can be anything.
1164     *
1165     * You can create an action file per http method by suffixing the action
1166     * name by the http method in lower case with a dot separating them. 
1167     * (eg: submit action for POST => submit.post.php)
1168     * The non-http-method specific file (ie without any suffix) will always
1169     * be executed before the http-method specific file and variables will
1170     * be forwarded from one to another.
1171     * 
1172     * @param string $action
1173     * @param string $method
1174     * @param array  $context
1175     * @return array
1176     */
1177    public static function executeFile($action, $method, $vars, $context)
1178    {
1179        // filenames
1180        $methodAction = $action . '.' . $method;
1181        $actionFilename = self::actionFilename($action);
1182        $methodActionFilename = self::actionFilename($methodAction);
1183        
1184        self::fireEvent('Atomik::Executefile', array(&$actionFilename, &$methodActionFilename, &$context));
1185        
1186        // checks if at least one of the action files or the view file is defined
1187        if ($actionFilename === false && $methodActionFilename === false) {
1188            return false;
1189        }
1190        
1191        $atomik = self::instance();
1192            
1193        // executes the global action
1194        if ($actionFilename !== false) {
1195            // executes the action in its own scope and fetches defined variables
1196            list($content, $vars) = $atomik->scoped($actionFilename, $vars);
1197            echo $content;
1198        }
1199        
1200        // executes the method specific action
1201        if ($methodActionFilename !== false) {
1202            // executes the action in its own scope and fetches defined variables
1203            list($content, $vars) = $atomik->scoped($methodActionFilename, $vars);
1204            echo $content;
1205        }
1206        
1207        return $vars;
1208    }
1209    
1210    /**
1211     * Prevents the view of the actionfrom which it's called to be rendered
1212     */
1213    public static function noRender()
1214    {
1215        if (count(self::$execContexts)) {
1216            self::$execContexts[count(self::$execContexts) - 1]['view'] = false;
1217        }
1218    }
1219    
1220    /**
1221     * Modifies the view associted to the action from which it's called
1222     * 
1223     * @param string $view View name
1224     */
1225    public static function setView($view)
1226    {
1227        if (count(self::$execContexts)) {
1228            self::$execContexts[count(self::$execContexts) - 1]['view'] = $view;
1229        }
1230    }
1231    
1232    /**
1233     * Disables the layout
1234     * 
1235     * @param bool $disable Whether to disable the layout
1236     */
1237    public static function disableLayout($disable = true)
1238    {
1239        self::set('app/disable_layout', $disable);
1240    }
1241    
1242    
1243    /* -------------------------------------------------------------------------------------------
1244     *  Views
1245     * ------------------------------------------------------------------------------------------ */
1246    
1247    /**
1248     * Renders a view
1249     * 
1250     * Searches for a file called after the view inside
1251     * directories configured in atomik/dirs/views. If no file is found, an 
1252     * exception is thrown unless $triggerError is false.
1253     *
1254     * @param string $view The view name
1255     * @param array $vars An array containing key/value pairs that will be transformed to variables accessible inside the view
1256     * @param array $dirs Directories where view files are stored
1257     * @return string|bool
1258     */
1259    public static function render($view, $vars = array(), $dirs = null)
1260    {
1261        if ($dirs === null) {
1262            $dirs = self::get('atomik/dirs/views');
1263        }
1264        
1265        self::fireEvent('Atomik::Render::Start', array(&$view, &$vars, &$dirs, &$triggerError));
1266        
1267        // view filename
1268        if (($filename = self::viewFilename($view, $dirs)) === false) {
1269            self::trigger404('View ' . $view . ' not found');
1270        }
1271        
1272        self::fireEvent('Atomik::Render::Before', array(&$view, &$vars, &$filename, $triggerError));
1273        
1274        $output = self::renderFile($filename, $vars);
1275        
1276        self::fireEvent('Atomik::Render::After', array($view, &$output, $vars, $filename, $triggerError));
1277        
1278        return $output;
1279    }
1280    
1281    /**
1282     * Renders a file using a filename which will not be resolved.
1283     *
1284     * @param string $filename Filename
1285     * @param array $vars An array containing key/value pairs that will be transformed to variables accessible inside the file
1286     * @return string The output of the rendered file
1287     */
1288    public static function renderFile($filename, $vars = array())
1289    {
1290        self::fireEvent('Atomik::Renderfile::Before', array(&$filename, &$vars));
1291        
1292        if (($callback = self::get('app/views/engine', false)) !== false) {
1293            if (!is_callable($callback)) {
1294                throw new Atomik_Exception('The specified rendering engine callback cannot be called');
1295            }
1296            $output = $callback($filename, $vars);
1297            
1298        } else {
1299            list($output, $vars) = self::instance()->scoped($filename, $vars);
1300        }
1301        
1302        self::fireEvent('Atomik::Renderfile::After', array($filename, &$output, $vars));
1303        
1304        return $output;
1305    }
1306    
1307    /**
1308     * Renders a layout
1309     * 
1310     * @param string $layout Layout name
1311     * @param string $content The content that will be available in the layout in the $contentForLayout variable
1312     * @param array $vars An array containing key/value pairs that will be transformed to variables accessible inside the layout
1313     * @param array $dirs Directories where to search for layouts
1314     * @return string
1315     */
1316    public static function renderLayout($layout, $content, $vars = array(), $dirs = null)
1317    {
1318        if ($dirs === null) {
1319            $dirs = self::get('atomik/dirs/layouts');
1320        }
1321        
1322        if (is_array($layout)) {
1323            foreach (array_reverse($layout) as $lay) {
1324                $content = self::renderLayout($lay, $content, $vars, $dirs);
1325            }
1326            return $content;
1327        }
1328        
1329        $appLayout = self::delete('app/layout');
1330        self::set('app/layout', array($layout));
1331        
1332        do {
1333            $layout = array_shift(self::getRef('app/layout'));
1334            self::fireEvent('Atomik::Renderlayout', array(&$layout, &$content, &$vars, &$dirs));
1335            $vars['contentForLayout'] = $content;
1336            $content = self::render($layout, $vars, $dirs);
1337        } while (count(self::get('app/layout')));
1338        
1339        self::set('app/layout', $appLayout);
1340        return $content;
1341    }
1342    
1343    
1344    /* -------------------------------------------------------------------------------------------
1345     *  Helpers
1346     * ------------------------------------------------------------------------------------------ */
1347    
1348    /**
1349     * Loads an helper file
1350     * 
1351     * @param string $helperName
1352     * @param array $dirs Directories where to search for helpers
1353     */
1354    public static function loadHelper($helperName, $dirs = null)
1355    {
1356        if (isset(self::$loadedHelpers[$helperName])) {
1357            return;
1358        }
1359        
1360        if ($dirs === null) {
1361            $dirs = self::get('atomik/dirs/helpers');
1362        }
1363        
1364        self::fireEvent('Atomik::Loadhelper::Before', array(&$helperName, &$dirs));
1365        
1366        if (($filename = self::path($helperName . '.php', $dirs)) === false) {
1367            throw new Atomik_Exception('Helper ' . $helperName . ' not found');
1368        }
1369        
1370        include $filename;
1371    
1372        if (!function_exists($helperName)) {
1373            // searching fo…

Large files files are truncated, but you can click here to view the full file