← Back to team overview

kolibri-discuss team mailing list archive

[Merge] lp:~frode-danielsen/kolibri/config+autoload into lp:kolibri

 

Frode Danielsen has proposed merging lp:~frode-danielsen/kolibri/config+autoload into lp:kolibri.

Requested reviews:
    Kolibri Dev (kolibri-dev)
-- 
https://code.launchpad.net/~frode-danielsen/kolibri/config+autoload/+merge/5563
Your team Kolibri Discuss is subscribed to branch lp:kolibri.
=== removed file 'examples/wishlist/conf/config.php'
--- examples/wishlist/conf/config.php	2009-04-07 23:17:37 +0000
+++ examples/wishlist/conf/config.php	1970-01-01 00:00:00 +0000
@@ -1,72 +0,0 @@
-<?php
-/*
- * General configuration. Put whatever application-specific configuration you like here. Each setting can be
- * get by calling Config::get('key'), where key is the setting you want to return, i.e. 'mail'.
- */
-$config = array(
-		'webRoot'    => 'http://localhost', // Must be absolute URI including scheme. No trailing slash!
-		'staticRoot' => '/static',          // URI of static resources (can be another host as http://static.example.com)
-		'locale'     => 'en_US.utf8',
-		'logging'    => array(
-			'enabled'  => false,   // When logging is disabled, errors are outputted directly. When enabled...
-			'logFile'  => '',      // ...they can be logged to a file
-			'logEmail' => ''       // ...and/or an email address (make sure you config 'mail' for this to work)
-		),
-		/*
-		 * Database configuration. 'type' is mandatory, while implementations define other settings.
-		 */
-		'db'        => array(
-			'type'          => 'Sqlite', // We also support PostgreSql, but not for this demo app
-			'database'      => APP_PATH . '/db/wishlist.db' // Filename for SQLite (writable by Apache)
-		),
-		/*
-		 * Configure your e-mail details for the MailService library used to send e-mails.
-		 */
-		'mail'      => array(
-			'from'          => '',
-			'from_name'     => '',
- 			'smtp_auth'     => false,
-	 		'smtp_host'     => '',
- 			'smtp_username' => '',
- 			'smtp_password' => ''
-		)
-);
-
-/*
- * Defines which action mappers to use for the specified URIs. The first match is used - the entire
- * array is not necessarily iterated.
- */
-$actionMappers = array(
-		'*' => 'DefaultActionMapper'
-);
-
-/*
- * $interceptorMappings defines which interceptors are to be invoked at a given URI. Wildcard mapping
- * is supported. You specify interceptors by their name configured in
- * <code>interceptors.php</code>, which can either refer to single interceptors or interceptor
- * stacks. The interceptors must be wrapped in an array, and you may reference both single interceptors
- * and interceptor stacks in the same mapping.
- * 
- * You can exclude certain interceptors from being invoked at specific URIs by prefixing the
- * interceptor name with a ! (exclamation mark). For instance, you may have an authentication
- * interceptor mapped to /admin/* (everything within /admin), but want to leave /admin/login open
- * to the public (after all, users must be allowed to log in). This can be done by these mappings:
- * 
- *    '/admin/*'     => array('auth'),
- *    '/admin/login' => array('!auth')
- * 
- * Order is of significance when mapping interceptors. If you were to define the excluding mapping
- * in this example before the regular inclusive mapping, it would not work as advertised above.
- * The wild-card mapping within /admin would then override the specific /admin/login, and the
- * exclude-mapping would not have any effect.
- */
-$interceptorMappings = array(
-		'*' => array('defaultStack')
-);
-
-/*
- * You may override any interceptor specific settings you like. For example, to specify a custom model for the
- * user object, you could do the following (see kolibri/conf/interceptors.php for more).
- */
-//$interceptors['auth']['AuthInterceptor']['userModel'] = 'MyUser';
-?>

=== added file 'examples/wishlist/conf/development.ini'
--- examples/wishlist/conf/development.ini	1970-01-01 00:00:00 +0000
+++ examples/wishlist/conf/development.ini	2009-04-14 21:48:11 +0000
@@ -0,0 +1,1 @@
+; Empty for this simple example application.
\ No newline at end of file

=== added file 'examples/wishlist/conf/production.ini'
--- examples/wishlist/conf/production.ini	1970-01-01 00:00:00 +0000
+++ examples/wishlist/conf/production.ini	2009-04-14 21:48:11 +0000
@@ -0,0 +1,41 @@
+; Simple configuration of the wishlist application. This is the only configuration file
+; loaded in the production environment, but it will also be loaded as a base configuration
+; in development and test environments. Simply create files called development.ini
+; and test.ini to override any configuration settings for those environments. A good idea
+; is to use separate databases for production and development. A separate database is required
+; in the test environment (if you use databases in general).
+
+[app]
+webRoot = "http://localhost";	; Must be absolute URI including scheme. No trailing slash!
+staticRoot = "/static"			; URI of static resources (ie. another host: http://static.example.com)
+locale = "en_US.utf8"
+
+; Section defining where to log exceptions and errors to.
+
+[logging]
+level = Off				; When log level is Off, errors are output directly. When set to On...
+file = 					; ...they can be logged to a file (must be writable by Apache)
+email = 				; ...and/or to an email address (make sure to configure 'mail' as well)
+
+; Database configuration. 'type' is mandatory, while implementations can define other settings.
+
+[database]
+type = "Sqlite"						; We also support PostgreSql, but not for this demo app
+name = APP_PATH "/db/wishlist.db"	; Filename for SQLite (writable by Apache)
+
+; Configure your e-mail details for the MailService library used to send e-mails.
+
+[mail]
+from.email = 
+from.name = 
+smtp.auth = Off
+smtp.host = 
+smtp.username = 
+smtp.password = 
+
+; Section defining which interceptors are to be invoked at a given URI. Wildcard mapping
+; is supported. See production.ini in src/conf/ for more details on how this works and
+; info about other parts of interceptors you can configure.
+
+[interceptors]
+* = "defaultStack"

=== modified file 'examples/wishlist/views/error.php'
--- examples/wishlist/views/error.php	2009-04-03 16:04:34 +0000
+++ examples/wishlist/views/error.php	2009-04-14 21:48:11 +0000
@@ -23,9 +23,10 @@
 		<div id="error">
 <?php
 /*
- * If logging is disable, we display detailed error.
+ * If logging is disabled we display detailed error, but only if we're not in production mode.
  */
-if (!isset($config['logging']['enabled']) || !$config['logging']['enabled']):
+if (Config::getMode() != Config::PRODUCTION
+		&& !isset($config['logging']['level']) || $config['logging']['level'] == false):
 ?>
 				<h1><?php echo get_class($exception) ?></h1>
 				<p id="message">

=== modified file 'src/actions/ValidationAware.php'
--- src/actions/ValidationAware.php	2009-04-12 02:11:21 +0000
+++ src/actions/ValidationAware.php	2009-04-14 21:28:43 +0000
@@ -14,6 +14,6 @@
 	 * Called when validation failed. A Result object must be returned, which will be rendered
 	 * instead of continuing request processing.
 	 */
-	public function validationFailed () {}
+	public function validationFailed ();
 }
 ?>

=== modified file 'src/conf/autoload.php'
--- src/conf/autoload.php	2009-04-12 01:09:55 +0000
+++ src/conf/autoload.php	2009-04-12 02:36:42 +0000
@@ -1,10 +1,10 @@
 <?php
 /*
- * Defines classes that may be loaded upon its first use by __autoload(). They are not automatically
- * loaded unless they are actually used. See <code>UtilsInterceptor</code> for automatic loading of utils.
+ * Defines classes that may be loaded upon its first use by Autoloader::load(). They are not
+ * automatically loaded unless they are actually used. See <code>UtilsInterceptor</code> for
+ * automatic loading of utils.
  */
 $autoloadClasses = array(
-		'DefaultActionMapper'    => '/core/DefaultActionMapper.php',
 		'ActionSupport'          => '/actions/ActionSupport.php',
 		'AuthAware'              => '/actions/AuthAware.php',
 		'MessageAware'           => '/actions/MessageAware.php',
@@ -47,6 +47,7 @@
 		'SmartyResult'           => '/results/SmartyResult.php',
 		'TextResult'             => '/results/TextResult.php',
 		'XsltResult'             => '/results/XsltResult.php',
+		'Utils'                  => '/core/Utils.php',
 		'Validator'              => '/validation/Validator.php',
 		'ValidationHelper'       => '/validation/ValidationHelper.php');
 ?>

=== removed file 'src/conf/config.php'
--- src/conf/config.php	2009-04-07 23:17:37 +0000
+++ src/conf/config.php	1970-01-01 00:00:00 +0000
@@ -1,74 +0,0 @@
-<?php
-/*
- * Sample general configuration file. Copy this into your conf/ directory within your application. You may put 
- * whatever application-specific configuration you like here. Each setting can bee get by calling
- * Config::get('key'), where key is the setting you want to return, i.e. 'mail'.
- */
-$config = array(
-		'webRoot'    => 'http://localhost', // Must be absolute URI including scheme. No trailing slash!
-		'staticRoot' => '/static',          // URI of static resources (can be another host as http://static.example.com)
-		'locale'     => 'en_US.utf8',
-		'logging'    => array(
-			'enabled'  => false
-		),	
-		/*
-		 * Database configuration. 'type' is mandatory, while implementations can define other settings.
-		 */
-		'db'        => array(
-			'type'          => 'PostgreSql', // Or Sqlite
-			'database'      => '',           // Database name for PostgreSql, file for SQLite (writable by Apache)
-			'host'          => 'localhost',  // Not relevant for Sqlite
-			'username'      => '',           // --"--
-			'password'      => ''            // --"--
-		),
-		/*
-		 * Configure your e-mail details for the MailService library used to send e-mails.
-		 */
-		'mail'      => array(
-			'from'          => '',
-			'from_name'     => '',
- 			'smtp_auth'     => false,
-	 		'smtp_host'     => '',
- 			'smtp_username' => '',
- 			'smtp_password' => ''
-		)
-);
-
-/*
- * Defines which action mappers to use for the specified URIs. The first match is used - the entire
- * array is not necessarily iterated.
- */
-$actionMappers = array(
-		'*' => 'DefaultActionMapper'
-);
-
-/*
- * $interceptorMappings defines which interceptors are to be invoked at a given URI. Wildcard mapping
- * is supported. You specify interceptors by their name configured in
- * <code>interceptors.php</code>, which can either refer to single interceptors or interceptor
- * stacks. The interceptors must be wrapped in an array, and you may reference both single interceptors
- * and interceptor stacks in the same mapping.
- * 
- * You can exclude certain interceptors from being invoked at specific URIs by prefixing the
- * interceptor name with a ! (exclamation mark). For instance, you may have an authentication
- * interceptor mapped to /admin/* (everything within /admin), but want to leave /admin/login open
- * to the public (after all, users must be allowed to log in). This can be done by these mappings:
- * 
- *    '/admin/*'     => array('auth'),
- *    '/admin/login' => array('!auth')
- * 
- * Order is of significance when mapping interceptors. If you were to define the excluding mapping
- * in this example before the regular inclusive mapping, it would not work as advertised above.
- * The wild-card mapping within /admin would then override the specific /admin/login, and the
- * exclude-mapping would not have any effect.
- */
-$interceptorMappings = array(
-		'*' => array('defaultStack')
-);
-
-/*
- * You may override any interceptor specific settings you like. For example, to specify a custom model for the
- * user object, you could do the following (see kolibri/conf/interceptors.php for more).
- */
-//$interceptors['auth']['AuthInterceptor']['userModel'] = 'MyUser';
-?>

=== added file 'src/conf/development.ini'
--- src/conf/development.ini	1970-01-01 00:00:00 +0000
+++ src/conf/development.ini	2009-04-14 21:28:43 +0000
@@ -0,0 +1,5 @@
+; Application configuration file for development mode, the default environment mode
+; for Kolibri. You only need to override configuration values from production.ini,
+; if any, here. Ie. if you use the same database name, username and password only
+; on different hosts between production and development mode, all you need to override
+; is the 'host' value in the [database] section.
\ No newline at end of file

=== modified file 'src/conf/interceptors.php'
--- src/conf/interceptors.php	2009-04-08 15:34:28 +0000
+++ src/conf/interceptors.php	2009-04-14 21:28:43 +0000
@@ -7,17 +7,9 @@
 $interceptors = array(
 		'message'     => 'MessageInterceptor',
 		'validation'  => 'ValidationInterceptor',
-		'error'       => array(
-				'ErrorInterceptor' => array('result' => 'PhpResult', 'view' => '/error')
-		),
+		'error'       => 'ErrorInterceptor',
 		'session'     => 'SessionInterceptor',
-		'auth'        => array(
-				'AuthInterceptor'  => array(
-						'userModel' => 'AuthUser',
-						'userKey'   => 'user',
-						'loginUri'  => '/login'
-				)
-		),
+		'auth'        => 'AuthInterceptor',
 		'model'       => 'ModelInterceptor',
 		'params'      => 'ParametersInterceptor',
 		'upload'      => 'UploadInterceptor',
@@ -26,6 +18,22 @@
 );
 
 /*
+ * Defines the default settings for interceptors. Can be overriden in applications in the
+ * [interceptors.settings] section of a ini file.
+ */
+$interceptorSettings = array(
+	'error' => array(
+		'result' => 'PhpResult',
+		'view'   => '/error'
+	),
+	'auth'  => array(
+		'userModel' => 'AuthUser',
+		'userKey'   => 'user',
+		'loginUri'  => '/login'
+	)	
+);
+
+/*
  * Defines stacks of interceptors. This makes it easy to group together several interceptors, which just as
  * interceptor names, can be referenced directly in <code>main.php</code> or your own
  * <code>config.php</code>.

=== added file 'src/conf/production.ini'
--- src/conf/production.ini	1970-01-01 00:00:00 +0000
+++ src/conf/production.ini	2009-04-14 21:48:11 +0000
@@ -0,0 +1,84 @@
+; Sample general configuration file for the production environment. Copy this into your
+; application's conf/ directory. You may put whatever application-specific configuration
+; you like here. Each setting can be fetched by calling Config::get('key'), where key is
+; the setting you want to return, ie. 'mail'.
+
+[app]
+webRoot = 				; Must be absolute URI including scheme. No trailing slash!
+staticRoot = "/static"	; URI of static resources (ie. another host: http://static.example.com)
+locale = "en_US.utf8"
+
+; Section defining where to log exceptions and errors to. Logging is automatically enabled
+; in the production environment, and disabled in development and testing (where they are
+; displayed instead).
+
+[logging]
+level = Off				; Currently only On or Off
+file = 					; Absolute path, needs to be writable by Apache
+email = 				; When defined, be sure to configure MailService below as well
+
+; Database configuration. 'type' is mandatory, while implementations can define other settings.
+
+[database]
+type = "PostgreSql"		; Or 'Sqlite'
+name = 					; Database name for PostgreSql, file for SQLite (writable by Apache)
+host = "localhost"		; Not relevant for Sqlite
+username = 				; --"--
+password = 				; --"--
+
+; Configure your e-mail details for the MailService library used to send e-mails.
+
+[mail]
+from.email = 
+from.name = 
+smtp.auth = Off
+smtp.host = 
+smtp.username = 
+smtp.password = 
+
+; Section defining which interceptors are to be invoked at a given URI. Wildcard mapping
+; is supported. You specify interceptors by their name configured in interceptors.php, which
+; can either refer to single interceptors or interceptor stacks. The interceptors must be
+; wrapped in an array, and you may reference both single interceptors and interceptor stacks
+; in the same mapping.
+; 
+; You can exclude certain interceptors from being invoked at specific URIs by prefixing the
+; interceptor name with a ! (exclamation mark). For instance, you may have an authentication
+; interceptor mapped to /admin/* (everything within /admin), but want to leave /admin/login open
+; to the public (after all, users must be allowed to log in). This can be done by these mappings:
+; 
+;   /admin/*     = "auth"
+;   /admin/login = "!auth"
+; 
+; Order is of significance when mapping interceptors, and should be from least to most specific
+; URI. If you were to define the excluding mapping in this example before the regular inclusive
+; mapping, it would not work as advertised above. The wild-card mapping within /admin
+; would then override the specific /admin/login, and the exclude-mapping would not have any
+; effect.
+
+[interceptors]
+* = "defaultStack"
+
+; You may override any interceptor specific settings you like. The example below specifies
+; the model class, session key and login URI for the authentication interceptor
+; (see src/conf/interceptors.php in for more settings).
+
+[interceptors.settings]
+; auth.userModel = "AuthUser"
+; auth.userKey = "user"
+; auth.loginUri = "/login"
+
+; You can define your own stacks of interceptors when you use a specific combination
+; of interceptors often. The name of a stack is used in exactly the same way as
+; the name of a single interceptor in the URI mapping above. There are two predefined
+; stacks, shown below.
+
+[interceptors.stacks]
+; defaultStack = "session, message, error, transaction, model, validation"
+; authStack = "session, message, error, transaction, auth, model, validation"
+
+; Section defining which action mappers to use for the specified URIs. The first match is
+; used - all definitions are not necessarily searched for the most specific match.
+
+; [actionmappers]
+; * = "DefaultActionMapper"

=== added file 'src/conf/test.ini'
--- src/conf/test.ini	1970-01-01 00:00:00 +0000
+++ src/conf/test.ini	2009-04-14 21:28:43 +0000
@@ -0,0 +1,4 @@
+; Application configuration file for test mode, the environment mode Kolibri uses
+; when you run your application tests. This configuration inherits values from
+; development.ini in the same way development.ini inherits from production.ini.
+; Usually all you need to configure here is a separate test database.
\ No newline at end of file

=== added file 'src/core/Autoloader.php'
--- src/core/Autoloader.php	1970-01-01 00:00:00 +0000
+++ src/core/Autoloader.php	2009-04-08 17:13:50 +0000
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Loads core framework classes and application models with their DAO classes as required.
+ */
+class Autoloader {
+	/**
+	 * Mapping of class names to files with their definitions.
+	 * @var array
+	 */
+	private static $classes = array();
+	
+	/**
+	 * Cache table with names of classes that have been loaded.
+	 * Only used for classes not in the autoload class mapping.
+	 * @var array
+	 */
+	private static $loaded = array();
+	
+	/**
+	 * Initializes the Autoloader with a mapping of class names to files,
+	 * and registers the autoload function with the PHP runtime.
+	 */
+	public static function initialize ($config) {
+		self::$classes = $config;
+		
+		spl_autoload_register(array('Autoloader', 'load'));
+	}
+	
+	/**
+	 * Kolibris autoload function. Handles autoloading of core framework classes
+	 * through a lookup mapping defined in conf/autoload.php. It also handles autoloading
+	 * of application model and DAO classes.
+	 */
+	public static function load ($className) {
+		// We optimize for core classes and check if that's what needs loading first.
+		if (isset(self::$classes[$className])) {
+			require(ROOT . self::$classes[$className]);
+		}
+		else if (!isset(self::$loaded[$className])) {
+			// DAO class names in Kolibri consists of the model name with 'Dao' appended.
+			if (substr($className, -3) == 'Dao') {
+				require(MODELS_PATH . "/dao/{$className}.php");
+			}
+			// If it's not a DAO class, see if it's a model class
+			else if (file_exists(MODELS_PATH . "/{$className}.php")) {
+				require(MODELS_PATH . "/{$className}.php");
+			}
+			// If it's not a core, model or DAO class we simply use the include_path
+			else {
+				require($className . '.php');
+			}
+			self::$loaded[$className] = true;
+		}
+	}
+}
+?>
\ No newline at end of file

=== modified file 'src/core/Config.php'
--- src/core/Config.php	2008-12-08 18:45:30 +0000
+++ src/core/Config.php	2009-04-14 21:28:43 +0000
@@ -1,11 +1,33 @@
 <?php
+require(ROOT . '/core/Autoloader.php');
+require(ROOT . '/core/ConfigHelper.php');
+
+// Define constants for application specific directories
+define('ACTIONS_PATH', APP_PATH . '/actions');
+define('MODELS_PATH', APP_PATH . '/models');
+define('VIEW_PATH', APP_PATH . '/views');
+
 /**
  * This class represents the configuration of the Kolibri framework.
  *
- * All configuration variables are easily availible through the static methods of this class.
+ * All configuration variables are easily available through the static methods of this class.
  */
 class Config {
 	/**
+	 * Constants for the different environment modes an application can be in.
+	 * Development is default, but can be changed through KOLIBRI_MODE environment variable.
+	 */
+	const PRODUCTION = 'production';
+	const DEVELOPMENT = 'development';
+	const TEST = 'test';
+	
+	/**
+	 * Current environment mode.
+	 * @var string
+	 */
+	private $mode;
+	
+	/**
 	 * General configuration settings.
 	 * @var array
 	 */
@@ -18,23 +40,29 @@
 	private $interceptorClasses;
 
 	/**
-	 * Interceptors mapped to actions [action path => interceptors]
-	 * @var array
-	 * */
+	 * Settings for interceptors.
+	 * @var array
+	 */
+	private $interceptorSettings;
+	
+	/**
+	 * Prepared list of URIs to unique set of interceptors.
+	 * @var array
+	 */
 	private $interceptorMappings;
-
-	/**
-	 * Defines the action mappers responding to specific URIs.
-	 * @var array
-	 */
-	private $actionMappers;
-
-	/**
-	 * Defines the validation configuration (validator classes and validation messages).
-	 * @var array
-	 */
-	private $validation;
-
+	
+	/**
+	 * Defines the validation classes.
+	 * @var array
+	 */
+	private $validationClasses;
+
+	/**
+	 * Defines validation error messages.
+	 * @var array
+	 */
+	private $validationMessages;
+	
 	/**
 	 * Singleton instance of this class.
 	 * @var Config
@@ -45,48 +73,86 @@
 	 * Private constructor which initializes the configuration. It is defined private as all
 	 * interaction with this class goes through static methods.
 	 */
-	private function __construct () {
+	private function __construct ($mode) {
+		$this->mode = $mode;
+
+		require(ROOT . '/conf/autoload.php');
 		require(ROOT . '/conf/interceptors.php');
 		require(ROOT . '/conf/validation.php');
-		require(APP_PATH . '/conf/config.php');
-
-		$this->config              = $config;
-		$this->actionMappers       = $actionMappers;
-		$this->interceptorClasses  = $interceptors;
-		$this->interceptorMappings = $interceptorMappings;
-		$this->validation          = array('classes' => $validators, 'messages' => $validationMessages);
-
-		/*
-		 * Loop through interceptor stacks. For each stack, add the stack to the regular interceptor
-		 * list with the correct interceptors attached. This makes it possible to use a stack just as
-		 * a single interceptor.
-		 */
-		foreach ($interceptorStacks as $name => $stack) {
-			foreach ($stack as $interceptor) {
-				/*
-				 * $interceptor must be the name of an existing interceptor. This gives us access
-				 * to the actual interceptor class within the stack.
-				 */
-				$this->interceptorClasses[$name][] = $this->interceptorClasses[$interceptor];
-			}
-		}
-
+
+		// Initialize the Kolibri class autoloader with classname-to-file mappings from conf/autoload.php
+		Autoloader::initialize($autoloadClasses);
+
+		// Load relevant app configuration depending on current environment mode
+		$this->config = ConfigHelper::loadApp($this->mode);
+		
+		/*
+		 * Extract all interceptor configurations from the loaded application
+		 * configuration. These configurations are irrelevant as normal configuration
+		 * values. They are instead merged with the default configuration and compiled
+		 * internally for use with the Dispatcher.
+		 */
+		if (isset($this->config['interceptors.stacks'])) {
+			$appInterceptorStacks = $this->config['interceptors.stacks'];
+			unset($this->config['interceptors.stacks']);
+		}
+		else $appInterceptorStacks = array();
+		
+		if (isset($this->config['interceptors.settings'])) {
+			$appInterceptorSettings = $this->config['interceptors.settings'];
+			unset($this->config['interceptors.settings']);
+		}
+		else $appInterceptorSettings = array();
+
+		if (isset($this->config['interceptors'])) {
+			$appInterceptorMappings = $this->config['interceptors'];
+			unset($this->config['interceptors']);
+		}
+		else $appInterceptorMappings = array();
+		
+		// Compile single index of interceptor classes, default stacks and application stacks
+		$this->interceptorClasses =
+				ConfigHelper::prepareInterceptors($interceptors, $interceptorStacks, $appInterceptorStacks);
+		// Merge default interceptor settings with any application specific settings
+		$this->interceptorSettings =
+				ConfigHelper::prepareInterceptorSettings($interceptors, $interceptorSettings,
+						$appInterceptorSettings);
+		/*
+		 * Optimize interceptor mappings by flattening stacks and filtering down to a unique
+		 * list of interceptors for each URI.
+		 */
+		$this->interceptorMappings =
+				ConfigHelper::prepareInterceptorMappings($this->interceptorClasses, $appInterceptorMappings);
+		
+		// Store validation configuration from conf/validation.php
+		$this->validationClasses = $validators;
+		$this->validationMessages = $validationMessages;
+	}
+	
+	/**
+	 * Initializes PHP settings based on the current configuration. This is done separately
+	 * from the constructor to support initalization after loading a stored instance
+	 * of the configuration (ie. serialized).
+	 */
+	private function init () {
 		$incPath = ROOT . '/lib';
-		if (isset($this->config['include_path'])) {
-			$incPath .= PATH_SEPARATOR . implode(PATH_SEPARATOR, $this->config['include_path']);
+		if (isset($this->config['includePath'])) {
+			$incPath .= PATH_SEPARATOR . implode(PATH_SEPARATOR, (array) $this->config['includePath']);
 		}
 		ini_set('include_path', ini_get('include_path') . PATH_SEPARATOR . $incPath);
-
+		
 		/*
 		 * Sets the current locale for date formatting et cetera
 		 * XXX: We leave LC_NUMERIC at default, as locales with comma as decimal seperator will
 		 * cause SQL queries with floating point values to fail. We should find a better solution...
 		 */
-		$envLocale = setlocale(LC_NUMERIC, 0);
-		setlocale(LC_ALL, $this->config['locale']);
-		setlocale(LC_NUMERIC, $envLocale);
+		if (isset($this->config['locale'])) {
+			$envLocale = setlocale(LC_NUMERIC, 0);
+			setlocale(LC_ALL, $this->config['locale']);
+			setlocale(LC_NUMERIC, $envLocale);
+		}
 	}
-
+	
 	/**
 	 * Returns an instance of this class. An existing instance is returned if one exists, else a new
 	 * instance is created.
@@ -95,32 +161,24 @@
 	 */
 	public static function getInstance () {
 		if (!isset(self::$instance)) {
-			self::$instance = new Config();
+			$mode = ConfigHelper::getMode();
+			// XXX: Unserialize cached config when appropriate depending on mode here
+			self::$instance = new Config($mode);
+			self::$instance->init();
 		}
 		return self::$instance;
 	}
 
 	/**
-	 * Loads a configuration file. The new configuration settings will take precidence if there any
-	 * conflicts with existing configuration settings.
+	 * Returns the environment mode Kolibri is currently running in.
 	 *
-	 * @param string $file	Configuration file to load (a PHP file).
-	 * @throws Exception	If no config file was specified, or it doesn't exist.
+	 * @return string Either Config::DEVELOPMENT, Config::TEST or Config::PRODUCTION.
 	 */
-	public static function load ($file) {
-		if (!empty($file) && is_file($file)) {
-			require($file);
-
-			if (is_array($config)) {
-				$instance = Config::getInstance();
-				$instance->config = array_merge($instance->config, $config);
-			}
-		}
-		else {
-			throw new Exception('No config-file specified');
-		}
+	public static function getMode () {
+		$instance = Config::getInstance();
+		return $instance->mode;
 	}
-
+	
 	/**
 	 * Returns the value of the configuration setting with the specified key, or <code>NULL</code> if
 	 * not found. If no key is supplied, all settings are returned.
@@ -139,57 +197,50 @@
 	/**
 	 * Returns the names of the action mappers defined for this application.
 	 *
-	 * @return array	Associative array with URIs mapped to action mappers.
+	 * @return mixed Associative array with URIs mapped to action mappers, or <code>NULL</code> when
+	 *               none is defined.
 	 */
 	public static function getActionMappers () {
 		$instance = Config::getInstance();
-		return $instance->actionMappers;
+		return (isset($instance->config['actionmappers']) ? $instance->config['actionmappers'] : null);
 	}
 
 	/**
 	 * Returns the interceptor mappings defined for this application.
 	 *
-	 * @return array	Associative array with action paths mapped to interceptor names.
+	 * @return array Associative array with action paths mapped to interceptor names.
 	 */
 	public static function getInterceptorMappings () {
 		$instance = Config::getInstance();
-		return $instance->interceptorMappings;
+		return (array) $instance->interceptorMappings;
 	}
 
 	/**
-	 * Returns an array with the class of an interceptor or classes of an interceptor stack.
+	 * Returns any settings defined for a specific interceptor, or all interceptor
+	 * settings.
 	 *
-	 * @param string $key	Key of interceptor or interceptor stack to get classes for.
-	 * @return array		Array of class names.
+	 * @param string $name Optional name of interceptor to return settings for.
+	 * @return array Associative array with settings for all interceptors or the one
+	 *               interceptor asked for.
 	 */
-	public static function getInterceptorClasses ($key) {
+	public static function getInterceptorSettings ($name = null) {
 		$instance = Config::getInstance();
-
-		if (!empty($instance->interceptorClasses[$key])) {
-			$class = $instance->interceptorClasses[$key];
-
-			/*
-			 * We must check if index 0 is not set in addition to the regular array-check, as $class can
-			 * be an interceptor definition with parameters (an array with a class name as the key and an
-			 * array of interceptor parameters as the value). We want to wrap those in an array as well,
-			 * hence the extra check.
-			 */
- 			 if (!is_array($class) || !isset($class[0])) {
-				return array($class);
-			}
-			return $class;
+		if ($name !== null) {
+			return (isset($instance->interceptorSettings[$name]) ?
+				$instance->interceptorSettings[$name] : null);
 		}
-		return array();
+		return (array) $instance->interceptorSettings;
 	}
 
 	/**
 	 * Returns an array of validation configuration.
+	 * XXX: Separate fetching of validation classes and messages.
 	 *
 	 * @return array		Array of validation configuration.
 	 */
 	public static function getValidationConfig () {
 		$instance = Config::getInstance();
-		return $instance->validation;
+		return array('classes' => $instance->validationClasses, 'messages' => $instance->validationMessages);
 	}
 }
 ?>

=== added file 'src/core/ConfigHelper.php'
--- src/core/ConfigHelper.php	1970-01-01 00:00:00 +0000
+++ src/core/ConfigHelper.php	2009-04-14 21:48:11 +0000
@@ -0,0 +1,230 @@
+<?php
+/**
+ * Helper class for reading environment variables, loading configuration files and parsing
+ * configuration values.
+ */
+class ConfigHelper {
+	/**
+	 * Determines and returns the current environment mode. Checks for an environment variable
+	 * named KOLIBRI_MODE and uses it's value if it's one of the three supported environment
+	 * modes. In any other case the default environment mode is development.
+	 *
+	 * @throws Exception If KOLIBRI_MODE contains an unsupported environment mode.
+	 */
+	public static function getMode () {
+		if (($envMode = getenv('KOLIBRI_MODE')) !== false) {
+			if ($envMode == Config::PRODUCTION
+					|| $envMode == Config::DEVELOPMENT
+					|| $envMode == Config::TEST) {
+				return $envMode;
+			}
+			else {
+				throw new Exception("Invalid environment mode in \$KOLIBRI_MODE: '$envMode'");
+			}
+		}
+
+		return Config::DEVELOPMENT;
+	}
+	
+	/**
+	 * Loads all application configuration for the current environment mode.
+	 * Configuration files are loaded for each environment in a hierarchy:
+	 *   Production -> Development -> Test
+	 * The production configuration will always be loaded, but overridden where
+	 * neccessary in development and test environments.
+	 *
+	 * @param string $runtimeMode Current environment mode, usually as determined through
+	 *                            ConfigHelper::getMode().
+	 * @return array Associative array with all configuration settings for the current
+	 *               environment mode.
+	 * @throws Exception If application runs in test mode but no database is configured
+	 *                   to replace the development database configuration.
+	 */
+	public static function loadApp ($runtimeMode) {
+		Utils::import('arrays');
+		
+		// Set up the cascading stack of configuration files depending on current mode
+		$configStack = array(Config::PRODUCTION);
+		if ($runtimeMode != Config::PRODUCTION) {
+			$configStack[] = Config::DEVELOPMENT;
+			
+			if ($runtimeMode == Config::TEST) {
+				$configStack[] = Config::TEST;
+			}
+		}
+		
+		$config = array();
+		foreach ($configStack as $configMode) {
+			$modeConfig = self::loadMode($configMode);
+			
+			/*
+			 * We want to prevent accidental use of the development or production database
+			 * in the test environment where all data is volatile.
+			 */
+			if ($configMode == Config::TEST) {
+				if (isset($config['database']) && !isset($modeConfig['database'])) {
+					throw new Exception('No test database configured to override development database.');
+				}
+			}
+			
+			// Merge config for a specific mode with previously loaded configuration recursively
+			$config = array_merge_recursive_distinct($config, $modeConfig);
+		}
+		
+		/*
+		 * The 'app' section in configuration files are automatically flattened to global
+		 * configuration values for convenience.
+		 */
+		if (isset($config['app'])) {
+			foreach ($config['app'] as $key => $value) {
+				$config[$key] = $value;
+			}
+			unset($config['app']);
+		}
+		
+		return $config;
+	}
+	
+	/**
+	 * Loads the application's configuration file for a specific environment mode.
+	 *
+	 * @param string $mode Either Config::PRODUCTION, Config::DEVELOPMENT or Config::TEST.
+	 * @throws Exception   If the configuration file for the mode does not exist, or
+	 *                     there was an error parsing the file (syntax error).
+	 */
+	public static function loadMode ($mode) {
+		$file = APP_PATH . "/conf/{$mode}.ini";
+		if (!file_exists($file)) {
+			throw new Exception("Application configuration file missing for {$mode} environment: $file");
+		}
+		
+		$config = @parse_ini_file($file, true);
+		if ($config === false) {
+			// Raise the PHP warning from syntax errors in configuration file to an Exception
+			$error = error_get_last();
+			throw new Exception($error['message']);
+		}
+		
+		return $config;
+	}
+
+	/**
+	 * Merges default interceptor stacks with the application's stacks. Then loops through
+	 * all interceptor stacks, adding each stack to the regular interceptor list with the
+	 * correct interceptors attached. This makes it possible to use a stack just as a single
+	 * interceptor in interceptor mappings.
+	 *
+	 * @param array $classes           Mapping of interceptor names to interceptor classes.
+	 * @param array $defaultStacks     Default interceptor stacks defined in Kolibri.
+	 * @param array $applicationStacks Interceptor stacks defined in the application's
+	 *                                 configuration files.
+	 * @return array Mapping of interceptors that also include stacks.
+	 * @throws Exception If a stack includes the name of a non-existing interceptor.
+	 */
+	public static function prepareInterceptors (array $classes, array $defaultStacks,
+			array $applicationStacks) {
+		// Parse stacks defined in application ini files
+		foreach ($applicationStacks as $name => $stack) {
+			$applicationStacks[$name] = preg_split('/,\s*/', $stack);
+		}
+		
+		$stacks = array_merge($defaultStacks, $applicationStacks);
+		foreach ($stacks as $name => $stack) {
+			/*
+			 * Reset stack in case another with the same name exists already,
+			 * we don't support merging of individual interceptors for stacks.
+			 */
+			$classes[$name] = array();
+			foreach ($stack as $interceptor) {
+				/*
+				 * $interceptor must be the name of an existing interceptor. This gives us access
+				 * to the actual interceptor class within the stack.
+				 */
+				if (isset($classes[$interceptor])) {
+					$classes[$name][$interceptor] = $classes[$interceptor];
+				}
+				else {
+					throw new Exception("Non-existing interceptor '$interceptor' used "
+							. "in stack '$name'");
+				}
+			}
+		}
+		
+		return $classes;
+	}
+	
+	/**
+	 * Parses and validates application specific interceptor settings, and merges them with
+	 * the default interceptor settings. Each application specific setting must have a name
+	 * including both the name of the interceptor and the property it defines a value for,
+	 * separated by a period. Ie. a setting for which model class to use for an authenticated
+	 * user would look like this:
+	 *   auth.userModel = MyUser
+	 *
+	 * @param array $classes             Mapping of interceptor names to interceptor classes.
+	 * @param array $defaultSettings     Kolibri's default settings for interceptors.
+	 * @param array $applicationSettings Application specific settings for interceptors.
+	 * @param array $settings Associative array with setting names and their values, grouped
+	 *                        under the name of the interceptor they relate to.
+	 * @throws Exception If an application specific setting's name is invalid or incomplete.
+	 */
+	public static function prepareInterceptorSettings (array $classes, array $defaultSettings,
+			array $applicationSettings) {
+		Utils::import('arrays');
+		
+		$settings = array();
+		foreach ($applicationSettings as $setting => $value) {
+			if (substr_count($setting, '.') == 1) {
+				list($interceptor, $setting) = explode('.', $setting);
+				if (isset($classes[$interceptor])) {
+					$settings[$interceptor][$setting] = $value;
+				}
+				else {
+					throw new Exception('Settings defined for non-existing interceptor '
+							. "'$interceptor' ($file)");
+				}
+			}
+			else {
+				throw new Exception("Invalid key '$setting' in interceptor settings ($file)");
+			}
+		}
+		
+		return array_merge_recursive_distinct($defaultSettings, $settings);
+	}
+	
+	/**
+	 * Prepares interceptor mappings by flattening interceptor stacks and translating
+	 * interceptor names to class names.
+	 *
+	 * @param array $interceptors        Complete list of interceptors and interceptor stacks.
+	 * @param array $applicationMappings Mapping of application URIs to interceptors.
+	 * @return array Map of application URIs => interceptors where stacks are flattened and
+	 *               interceptor names converted to their class names.
+	 */
+	public static function prepareInterceptorMappings (array $interceptors, array $applicationMappings) {
+		foreach ($applicationMappings as $uri => $names) {
+			$classes = array();
+			foreach (preg_split('/,\s*/', $names) as $name) {
+				if ($name{0} == '!') {
+					$prefix = '!';
+					$name = substr($name, 1);
+				}
+				else {
+					$prefix = '';
+				}
+				
+				if (is_array($interceptors[$name])) {
+					$classes = array_merge($classes, $interceptors[$name]);
+				}
+				else {
+					$classes[$name] = $prefix . $interceptors[$name];
+				}
+			}
+			
+			$applicationMappings[$uri] = $classes;
+		}
+
+		return $applicationMappings;
+	}
+}
+?>
\ No newline at end of file

=== modified file 'src/core/Dispatcher.php'
--- src/core/Dispatcher.php	2008-10-20 16:41:10 +0000
+++ src/core/Dispatcher.php	2009-04-14 21:28:43 +0000
@@ -1,4 +1,6 @@
 <?php
+require(ROOT . '/core/InterceptorFactory.php');
+
 /**
  * This class dispatches the request to the interceptors and action target of the request.
  * 
@@ -55,26 +57,16 @@
 
 			if (preg_match($uriMatch, $request->getUri()) == 1) {
 				/*
-				 * Current URI matches the mapping. We loop through each interceptor mapped, find their
-				 * actual classes and sets or unsets their use depending on the current URI mapping.
+				 * Current URI matches the mapping. We loop through each interceptor for the
+				 * matched URI and add or remove from the final stack depending on it's prefix.
 				 */
-				foreach ($interceptors as $name) {
-					// Check whether the current interceptor is to be used or negated
-					if (!($use = (substr($name, 0, 1) != '!'))) {
-						$name = substr($name, 1); // Strip ! from the name
+				foreach ($interceptors as $name => $class) {
+					// Only interceptor classes without a ! prefix should be in the stack
+					if (substr($class, 0, 1) != '!') {
+						$stack[$name] = $class;
 					}
-
-					// Gets the actual classes the name represents
-					$classes = Config::getInterceptorClasses($name);
-
-					// Loop through interceptor classes and set or unset its use
-					foreach ($classes as $class) {
-						if ($use) {
-							$stack[] = $class;
-						}
-						else {
-							unset($stack[array_search($class, $stack)]);
-						}
+					else {
+						unset($stack[$name]);
 					}
 				}
 			}

=== modified file 'src/core/InterceptorFactory.php'
--- src/core/InterceptorFactory.php	2008-10-20 16:41:10 +0000
+++ src/core/InterceptorFactory.php	2009-04-14 21:28:43 +0000
@@ -13,21 +13,16 @@
 	/**
 	 * Instantiates, initilizes and returns interceptors set to be used.
 	 *
-	 * @param array $interceptors	Interceptor classes to instantiate.
-	 * @return array				An array with instantiated interceptors.
+	 * @param array $interceptors Interceptor classes to instantiate.
+	 * @param array $settings     Associative array with settings for interceptors.
+	 * @return array An array with instantiated interceptors.
 	 */
-	public static function createInterceptors ($interceptors) {
+	public static function createInterceptors (array $interceptors) {
 		$stack = array(); // To hold instantiated interceptors
-
-		foreach ($interceptors as $class) {
-			if (is_array($class)) {
-				// $class contains parameters to be passed to the constructor
-				$actualClass = key($class);
-				$instance = new $actualClass(current($class));
-			}
-			else {
-				$instance = new $class();
-			}
+		
+		foreach ($interceptors as $name => $class) {
+			$settings = Config::getInterceptorSettings($name);
+			$instance = new $class($settings);
 
 			$instance->init();
 			$stack[] = $instance;

=== modified file 'src/core/RequestProcessor.php'
--- src/core/RequestProcessor.php	2008-10-20 16:41:10 +0000
+++ src/core/RequestProcessor.php	2009-04-12 02:36:42 +0000
@@ -1,4 +1,7 @@
 <?php
+require(ROOT . '/core/Request.php');
+require(ROOT . '/core/Dispatcher.php');
+
 /**
  * This is the main class of the TURBO framework, which is responsible for initializing the request
  * processing flow.
@@ -65,20 +68,30 @@
 	 */
 	private function findActionMapper () {
 		$actionMappers = Config::getActionMappers();
-
-		foreach ($actionMappers as $uri => $mapper) { // Loop through URIs/mappers
+		
+		if ($actionMappers === null) {
+			return 'DefaultActionMapper';
+		}
+		
+		$requestUri = $this->request->getUri();
+		foreach ($actionMappers as $uri => $mapper) {
 			// Replace star wildcard mappings with regex "any characters" mapping, to use regex
 			$uri = '#^' . str_replace('*', '.*?', $uri) . '$#';
 
-			if (preg_match($uri, $this->request->getUri()) == 1) {
+			if (preg_match($uri, $requestUri) == 1) {
 				if ($mapper != 'DefaultActionMapper') {
-					// Mapper is application-specific, include it (DefaultActionMapper is autoloadable)
-					require(APP_PATH . "/mappers/$mapper.php");
-				}
-
+					// Mapper is application-specific, rely on include_path for loading it
+					require("$mapper.php");
+				}
+				else {
+					require('/core/DefaultActionMapper.php');
+				}
+				
 				return $mapper;
 			}
 		}
+		
+		throw new Exception("No ActionMapper configured for requested URI: $requestUri");
 	}
 
 	/**

=== added file 'src/core/Utils.php'
--- src/core/Utils.php	1970-01-01 00:00:00 +0000
+++ src/core/Utils.php	2009-04-08 00:04:08 +0000
@@ -0,0 +1,14 @@
+<?php
+class Utils {
+	public static function import ($module) {
+		$path = ROOT . "/utils/{$module}.php";
+		
+		if (file_exists($path)) {
+			require_once($path);
+		}
+		else {
+			throw new Exception("Utility module '$module' does not exist.");
+		}
+	}
+}
+?>
\ No newline at end of file

=== modified file 'src/database/DatabaseFactory.php'
--- src/database/DatabaseFactory.php	2008-12-05 01:34:28 +0000
+++ src/database/DatabaseFactory.php	2009-04-12 02:36:42 +0000
@@ -27,9 +27,10 @@
 	 * @throws Exception      If the implementation the configuration specifies is not found.
 	 * @return DatabaseConnection
 	 */
-	public static function getConnection ($confName = 'db') {
-		if (!isset(self::$connections[$confName])) {
-			$dbConf = Config::get($confName);
+	public static function getConnection ($confName = null) {
+		$confKey = ($confName === null ? 'database' : "database.{$confName}");
+		if (!isset(self::$connections[$confKey])) {
+			$dbConf = Config::get($confKey);
 
 			if (!empty($dbConf)) {
 				$dbConnClass = $dbConf['type'] . 'Connection';
@@ -46,14 +47,14 @@
 					}
 				}
 
-				self::$connections[$confName] = new $dbConnClass($dbConf);
+				self::$connections[$confKey] = new $dbConnClass($dbConf);
 			}
 			else {
-				throw new DatabaseException('No database configured');
+				throw new DatabaseException("No database configuration section named '$confKey'");
 			}
 		}
 
-		return self::$connections[$confName];
+		return self::$connections[$confKey];
 	}
 }
 ?>

=== modified file 'src/database/PostgreSqlConnection.php'
--- src/database/PostgreSqlConnection.php	2009-04-08 22:39:04 +0000
+++ src/database/PostgreSqlConnection.php	2009-04-12 02:36:42 +0000
@@ -39,7 +39,7 @@
 		$this->host       = isset($conf['host']) ? $conf['host'] : null;
 		$this->username   = $conf['username'];
 		$this->password   = $conf['password'];
-		$this->database   = $conf['database'];
+		$this->database   = $conf['name'];
 		$this->autocommit = isset($conf['autocommit']) ? $conf['autocommit'] : false;
 	}
 

=== modified file 'src/database/SqliteConnection.php'
--- src/database/SqliteConnection.php	2009-04-09 16:33:12 +0000
+++ src/database/SqliteConnection.php	2009-04-12 02:36:42 +0000
@@ -30,7 +30,7 @@
 	 * @param array $conf Database configuration.
 	 */
 	public function __construct ($conf) {
-		$this->database   = $conf['database'];
+		$this->database   = $conf['name'];
 		$this->autocommit = isset($conf['autocommit']) ? $conf['autocommit'] : false;
 	}
 

=== modified file 'src/interceptors/AbstractInterceptor.php'
--- src/interceptors/AbstractInterceptor.php	2008-10-20 16:41:10 +0000
+++ src/interceptors/AbstractInterceptor.php	2009-04-14 21:28:43 +0000
@@ -11,9 +11,9 @@
 	 * the instance. This requires that the concrete interceptor implementation defines the properties as
 	 * protected or public.
 	 *
-	 * @param array $conf	Configuration for this interceptor, if any.
+	 * @param array $conf Configuration for this interceptor, if any.
 	 */
-	public function __construct ($conf = null) {
+	public function __construct (array $conf = null) {
 		if ($conf !== null) {
 			foreach ($conf as $param => $value) {
 				$this->$param = $value;

=== modified file 'src/interceptors/AuthInterceptor.php'
--- src/interceptors/AuthInterceptor.php	2008-10-20 16:41:10 +0000
+++ src/interceptors/AuthInterceptor.php	2009-04-14 21:28:43 +0000
@@ -30,7 +30,7 @@
 	 * Initialize this interceptor by making sure the user model has been included.
 	 */
 	public function init () {
-		import($this->userModel);
+		Autoloader::load($this->userModel);
 	}
 
 	/**

=== modified file 'src/interceptors/UtilsInterceptor.php'
--- src/interceptors/UtilsInterceptor.php	2008-10-20 16:41:10 +0000
+++ src/interceptors/UtilsInterceptor.php	2009-04-12 02:36:42 +0000
@@ -12,7 +12,7 @@
 		$utils = Config::get('loadUtils');
 		if (is_array($utils)) {
 			foreach ($utils as $util) {
-				import($util, 'util');
+				Utils::import($util);
 			}
 		}
 

=== modified file 'src/kolibri.php'
--- src/kolibri.php	2008-12-09 23:12:48 +0000
+++ src/kolibri.php	2009-04-08 17:13:50 +0000
@@ -1,84 +1,40 @@
 <?php
 /**
- * The entryway to the Kolibri framework. Essential core files are included, config initialized and
- * processing of the request is started.
- */
-
-/*
- * Defines the root directory of the framework. By default this in a directory named kolibri within the 
- * document root, but this can be changed at will.
- */
-define('ROOT', dirname(__FILE__) . '/kolibri');
-
-/*
- * Application specific directories. Modify to your setup if different from the default.
- */
-define('APP_PATH', dirname(__FILE__));
-define('ACTIONS_PATH', APP_PATH . '/actions');
-define('MODELS_PATH', APP_PATH . '/models');
-define('VIEW_PATH', APP_PATH . '/views');
+ * The HTTP gateway to the Kolibri framework. Essential core files are included, config initialized and
+ * processing of the HTTP request is started.
+ * The constants defined here, ROOT and APP_PATH, can be changed from their default values by
+ * using SetEnv in the .htaccess file or in an appropriate directive in httpd.conf.
+ */
+
+/*
+ * Defines the root directory of the Kolibri framework. By default this is a directory named
+ * 'kolibri' within the document root.
+ */
+if ($rootDir = getenv('KOLIBRI_ROOT')) {
+	define('ROOT', $rootDir);
+}
+else {
+	define('ROOT', dirname(__FILE__) . '/kolibri');
+}
+
+/*
+ * Defines the root directory for the application's actions, models, views etc. By default this is
+ * the same directory as this kolibri.php file.
+ */
+if ($appDir = getenv('KOLIBRI_APP')) {
+	define('APP_PATH', $appDir);
+}
+else {
+	define('APP_PATH', dirname(__FILE__));
+}
 
 // Require essential files. Others are loaded as needed.
 require(ROOT . '/core/Config.php');
 require(ROOT . '/core/RequestProcessor.php');
-require(ROOT . '/core/Dispatcher.php');
-require(ROOT . '/core/InterceptorFactory.php');
-require(ROOT . '/core/Request.php');
 
 // Init configuration
 Config::getInstance();
 
 $kolibri = new RequestProcessor();
 $kolibri->process();
-
-/**
- * Loads framework classes and application models as required.
- */
-function __autoload ($name) {
-	static $autoload;
-
-	if (!isset($autoload)) {
-		require(ROOT . '/conf/autoload.php');
-		$autoload = $autoloadClasses;
-	}
-
-	if (isset($autoload[$name])) {
-		require(ROOT . $autoload[$name]);
-	}
-	else {
-		// Class not specified in autoload.php, but may be available in include_path (i.e. /lib)
-		if (!import($name)) {
-			require($name . '.php');
-		}
-	}
-}
-
-/**
- * Loads model, DAO or util files. Used internally by __autoload(), or by the user for importing utils like 
- * this:
- *
- *   import('urls', 'util');
- */
-function import ($file, $type = 'model') {
-	static $files;
-
-	switch ($type) {
-		case 'model': $path = MODELS_PATH; break;
-		case 'dao': $path = MODELS_PATH . '/dao'; break;
-		case 'util': $path = ROOT . '/utils'; break;
-		default: throw new Exception("Could not import $file. Type $type is unknown.");
-	}
-
-	$filePath = $path . "/$file.php";
-	if (!isset($files[$filePath])) {
-		if (!file_exists($filePath)) {
-			return false;
-		}
-
-		$files[$filePath] = true;
-		require($filePath);
-	}
-
-	return true;
-}
 ?>

=== modified file 'src/lib/ErrorHandler.php'
--- src/lib/ErrorHandler.php	2009-04-03 16:04:34 +0000
+++ src/lib/ErrorHandler.php	2009-04-14 21:28:43 +0000
@@ -56,19 +56,19 @@
 		$data = array('exception' => $exception, 'action' => $this->action);
 		$logging = Config::get('logging');
 
-		if (!empty($logging) && isset($logging['enabled']) && $logging['enabled']) {
-			if (!empty($logging['logFile']) && is_writable($logging['logFile'])) {
+		if (!empty($logging) && isset($logging['level']) && $logging['level'] == true) {
+			if (!empty($logging['file']) && is_writable($logging['file'])) {
 				error_log($exception->getMessage() . " ({$exception->getFile()}:{$exception->getLine()})",
-					3, $logging['logFile']);
+					3, $logging['file']);
 			}
 			else {
 				error_log($exception->getMessage() . " ({$exception->getFile()}:{$exception->getLine()})");
 			}
 
-			if (!empty($logging['logEmail'])) {
+			if (!empty($logging['email'])) {
 				// Send email to admin
 				$email = $this->generateMail($exception);
-				$email->addRecipient($logging['logEmail']);
+				$email->addRecipient($logging['email']);
 				$mailer = new MailService();
 				$mailer->send($email);
 			}

=== modified file 'src/lib/MailService.php'
--- src/lib/MailService.php	2008-12-08 20:34:09 +0000
+++ src/lib/MailService.php	2009-04-12 02:36:42 +0000
@@ -30,16 +30,16 @@
 
 		$this->IsSmtp();
 		$this->PluginDir = ROOT . '/lib/phpmailer/';
-		$this->Host      = $conf['smtp_host'];
-		$this->SMTPAuth  = $conf['smtp_auth'];
+		$this->Host      = $conf['smtp.host'];
+		$this->SMTPAuth  = $conf['smtp.auth'];
 
-		if (isset($conf['smtp_port'])) {
-			$this->Port = $conf['smtp_port'];
+		if (isset($conf['smtp.port'])) {
+			$this->Port = $conf['smtp.port'];
 		}
 
 		if ($this->SMTPAuth) {
-			$this->Username = $conf['smtp_username'];
-			$this->Password = $conf['smtp_password'];
+			$this->Username = $conf['smtp.username'];
+			$this->Password = $conf['smtp.password'];
 		}
 
 		$this->CharSet  = 'utf-8';
@@ -82,8 +82,8 @@
 	public function send ($mail) {
 		if (empty($mail->from)) {
 			$conf = Config::get('mail');
-			$mail->from     = $conf['from'];
-			$mail->fromName = $conf['from_name'];
+			$mail->from     = $conf['from.email'];
+			$mail->fromName = $conf['from.name'];
 		}
 
 		$this->From     = $mail->from;

=== modified file 'src/models/DataAccessProxy.php'
--- src/models/DataAccessProxy.php	2009-01-30 03:44:24 +0000
+++ src/models/DataAccessProxy.php	2009-04-08 00:04:08 +0000
@@ -38,7 +38,7 @@
 	public function __construct ($modelProxy, $modelClass) {
 		$this->modelProxy	= $modelProxy;
 		$this->daoClass		= $modelClass . 'Dao';
-		import($this->daoClass, 'dao');
+		Autoloader::load($this->daoClass);
 
 		$reflection = new ReflectionClass($modelClass);
 		$this->modelPk = $reflection->getConstant('PK');
@@ -48,7 +48,7 @@
 	 * Imports the DAO we are proxying upon wakeup (i.e. if a proxied model is put in the session).
 	 */
 	public function __wakeup () {
-		import($this->daoClass, 'dao');
+		Autoloader::load($this->daoClass);
 	}
 
 	/**

=== modified file 'src/utils/arrays.php'
--- src/utils/arrays.php	2009-01-30 23:18:57 +0000
+++ src/utils/arrays.php	2009-04-12 02:36:42 +0000
@@ -20,6 +20,38 @@
 }
 
 /**
+ * A blend of PHP's built-in array_merge() and array_merge_recursive(). This function merges
+ * recursively for array values, but like array_merge() it will overwrite a value in the
+ * first array with the corresponding value from the second array for equal keys.
+ *
+ * @param array $first  Base array in which values will be merged into.
+ * @param array $second Array to merge values from.
+ * @param array …       Optional extra arrays to merge values from.
+ * @return array Array with merged values from all input arrays.
+ */
+function array_merge_recursive_distinct (array $first, array $second/*, array …*/) {
+	$merged = $first;
+
+	if (is_array($second)) {
+		foreach ($second as $key => $value) {
+			if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
+				$merged[$key] = array_merge_recursive_distinct($merged[$key], $value);
+			}
+			else {
+				$merged[$key] = $value;
+			}
+		}
+	}
+
+	if (func_num_args() > 2) {
+		$params = array_merge(array($merged), array_slice(func_get_args(), 2));
+		return call_user_func_array('array_merge_recursive_distinct', $params);
+	}
+	
+	return $merged;
+}
+
+/**
  * BETA
  * Converts a multi-dimensional array into a one-dimensional array. Items which are objects are
  * converted to arrays consisting of their properties and considered just another dimension. An 

=== modified file 'src/views/error.php'
--- src/views/error.php	2009-04-03 16:04:34 +0000
+++ src/views/error.php	2009-04-14 21:28:43 +0000
@@ -23,9 +23,10 @@
 		<div id="error">
 <?php
 /*
- * If logging is disable, we display detailed error.
+ * If logging is disabled we display detailed error, but only if we're not in production mode.
  */
-if (!isset($config['logging']['enabled']) || !$config['logging']['enabled']):
+if (Config::getMode() != Config::PRODUCTION
+		&& !isset($config['logging']['level']) || $config['logging']['level'] == false):
 ?>
 				<h1><?php echo get_class($exception) ?></h1>
 				<p id="message">


Follow ups