Implement Loader method chaining

Requested in issue #2165
Supersedes PR #2319
diff --git a/system/core/Loader.php b/system/core/Loader.php
index 28b9502..daf326f 100644
--- a/system/core/Loader.php
+++ b/system/core/Loader.php
@@ -174,13 +174,13 @@
 	 * @param	string	$library	Library name
 	 * @param	array	$params		Optional parameters to pass to the library class constructor
 	 * @param	string	$object_name	An optional object name to assign to
-	 * @return	void
+	 * @return	object
 	 */
 	public function library($library, $params = NULL, $object_name = NULL)
 	{
 		if (empty($library))
 		{
-			return;
+			return $this;
 		}
 		elseif (is_array($library))
 		{
@@ -189,7 +189,7 @@
 				$this->library($class, $params);
 			}
 
-			return;
+			return $this;
 		}
 
 		if ($params !== NULL && ! is_array($params))
@@ -198,6 +198,7 @@
 		}
 
 		$this->_ci_load_class($library, $params, $object_name);
+		return $this;
 	}
 
 	// --------------------------------------------------------------------
@@ -210,13 +211,13 @@
 	 * @param	string	$model		Model name
 	 * @param	string	$name		An optional object name to assign to
 	 * @param	bool	$db_conn	An optional database connection configuration to initialize
-	 * @return	void
+	 * @return	object
 	 */
 	public function model($model, $name = '', $db_conn = FALSE)
 	{
 		if (empty($model))
 		{
-			return;
+			return $this;
 		}
 		elseif (is_array($model))
 		{
@@ -224,7 +225,8 @@
 			{
 				is_int($key) ? $this->model($value, '', $db_conn) : $this->model($key, $value, $db_conn);
 			}
-			return;
+
+			return $this;
 		}
 
 		$path = '';
@@ -246,7 +248,7 @@
 
 		if (in_array($name, $this->_ci_models, TRUE))
 		{
-			return;
+			return $this;
 		}
 
 		$CI =& get_instance();
@@ -283,7 +285,7 @@
 
 			$CI->$name = new $model();
 			$this->_ci_models[] = $name;
-			return;
+			return $this;
 		}
 
 		// couldn't find the model
@@ -300,8 +302,8 @@
 	 * @param	bool	$query_builder	Whether to enable Query Builder
 	 *					(overrides the configuration setting)
 	 *
-	 * @return	void|object|bool	Database object if $return is set to TRUE,
-	 *					FALSE on failure, void in any other case
+	 * @return	object|bool	Database object if $return is set to TRUE,
+	 *					FALSE on failure, CI_Loader instance in any other case
 	 */
 	public function database($params = '', $return = FALSE, $query_builder = NULL)
 	{
@@ -327,6 +329,7 @@
 
 		// Load the DB class
 		$CI->db =& DB($params, $query_builder);
+		return $this;
 	}
 
 	// --------------------------------------------------------------------
@@ -336,7 +339,7 @@
 	 *
 	 * @param	object	$db	Database object
 	 * @param	bool	$return	Whether to return the DB Utilities class object or not
-	 * @return	void|object
+	 * @return	object
 	 */
 	public function dbutil($db = NULL, $return = FALSE)
 	{
@@ -358,6 +361,7 @@
 		}
 
 		$CI->dbutil = new $class($db);
+		return $this;
 	}
 
 	// --------------------------------------------------------------------
@@ -367,7 +371,7 @@
 	 *
 	 * @param	object	$db	Database object
 	 * @param	bool	$return	Whether to return the DB Forge class object or not
-	 * @return	void|object
+	 * @return	object
 	 */
 	public function dbforge($db = NULL, $return = FALSE)
 	{
@@ -401,6 +405,7 @@
 		}
 
 		$CI->dbforge = new $class($db);
+		return $this;
 	}
 
 	// --------------------------------------------------------------------
@@ -415,7 +420,7 @@
 	 *				to be extracted for use in the view
 	 * @param	bool	$return	Whether to return the view output
 	 *				or leave it to the Output class
-	 * @return	void|string
+	 * @return	object|string
 	 */
 	public function view($view, $vars = array(), $return = FALSE)
 	{
@@ -429,7 +434,7 @@
 	 *
 	 * @param	string	$path	File path
 	 * @param	bool	$return	Whether to return the file output
-	 * @return	void|string
+	 * @return	object|string
 	 */
 	public function file($path, $return = FALSE)
 	{
@@ -448,7 +453,7 @@
 	 *					An associative array or object containing values
 	 *					to be set, or a value's name if string
 	 * @param 	string	$val	Value to set, only used if $vars is a string
-	 * @return	void
+	 * @return	object
 	 */
 	public function vars($vars, $val = '')
 	{
@@ -466,6 +471,8 @@
 				$this->_ci_cached_vars[$key] = $val;
 			}
 		}
+
+		return $this;
 	}
 
 	// --------------------------------------------------------------------
@@ -475,11 +482,12 @@
 	 *
 	 * Clears the cached variables.
 	 *
-	 * @return  void
+	 * @return  object
 	 */
 	public function clear_vars()
 	{
 		$this->_ci_cached_vars = array();
+		return $this;
 	}
 
 	// --------------------------------------------------------------------
@@ -517,7 +525,7 @@
 	 * Helper Loader
 	 *
 	 * @param	string|string[]	$helpers	Helper name(s)
-	 * @return	void
+	 * @return	object
 	 */
 	public function helper($helpers = array())
 	{
@@ -574,6 +582,8 @@
 				show_error('Unable to load the requested file: helpers/'.$helper.'.php');
 			}
 		}
+
+		return $this;
 	}
 
 	// --------------------------------------------------------------------
@@ -586,11 +596,11 @@
 	 *
 	 * @uses	CI_Loader::helper()
 	 * @param	string|string[]	$helpers	Helper name(s)
-	 * @return	void
+	 * @return	object
 	 */
 	public function helpers($helpers = array())
 	{
-		$this->helper($helpers);
+		return $this->helper($helpers);
 	}
 
 	// --------------------------------------------------------------------
@@ -602,18 +612,19 @@
 	 *
 	 * @param	string|string[]	$files	List of language file names to load
 	 * @param	string		Language name
-	 * @return	void
+	 * @return	object
 	 */
 	public function language($files, $lang = '')
 	{
 		$CI =& get_instance();
-
 		is_array($files) OR $files = array($files);
 
 		foreach ($files as $langfile)
 		{
 			$CI->lang->load($langfile, $lang);
 		}
+
+		return $this;
 	}
 
 	// --------------------------------------------------------------------
@@ -646,8 +657,8 @@
 	 * @param	array		$params		Optional parameters to pass to the driver
 	 * @param	string		$object_name	An optional object name to assign to
 	 *
-	 * @return	void|object|bool	Object or FALSE on failure if $library is a string
-	 *					and $object_name is set. void otherwise.
+	 * @return	object|bool	Object or FALSE on failure if $library is a string
+	 *				and $object_name is set. CI_Loader instance otherwise.
 	 */
 	public function driver($library, $params = NULL, $object_name = NULL)
 	{
@@ -657,10 +668,10 @@
 			{
 				$this->driver($driver);
 			}
-			return;
-		}
 
-		if ($library === '')
+			return $this;
+		}
+		elseif (empty($library))
 		{
 			return FALSE;
 		}
@@ -696,7 +707,7 @@
 	 *
 	 * @param	string	$path		Path to add
 	 * @param 	bool	$view_cascade	(default: TRUE)
-	 * @return	void
+	 * @return	object
 	 */
 	public function add_package_path($path, $view_cascade = TRUE)
 	{
@@ -711,6 +722,8 @@
 		// Add config file path
 		$config =& $this->_ci_get_component('config');
 		$config->_config_paths[] = $path;
+
+		return $this;
 	}
 
 	// --------------------------------------------------------------------
@@ -738,7 +751,7 @@
 	 * added path will be removed removed.
 	 *
 	 * @param	string	$path	Path to remove
-	 * @return	void
+	 * @return	object
 	 */
 	public function remove_package_path($path = '')
 	{
@@ -780,6 +793,8 @@
 		$this->_ci_model_paths = array_unique(array_merge($this->_ci_model_paths, array(APPPATH)));
 		$this->_ci_view_paths = array_merge($this->_ci_view_paths, array(APPPATH.'views/' => TRUE));
 		$config->_config_paths = array_unique(array_merge($config->_config_paths, array(APPPATH)));
+
+		return $this;
 	}
 
 	// --------------------------------------------------------------------
@@ -795,7 +810,7 @@
 	 * @used-by	CI_Loader::view()
 	 * @used-by	CI_Loader::file()
 	 * @param	array	$_ci_data	Data to load
-	 * @return	void
+	 * @return	object
 	 */
 	protected function _ci_load($_ci_data)
 	{
@@ -919,6 +934,8 @@
 			$_ci_CI->output->append_output(ob_get_contents());
 			@ob_end_clean();
 		}
+
+		return $this;
 	}
 
 	// --------------------------------------------------------------------
diff --git a/tests/codeigniter/core/Loader_test.php b/tests/codeigniter/core/Loader_test.php
index 8fbeaec..799bcd9 100644
--- a/tests/codeigniter/core/Loader_test.php
+++ b/tests/codeigniter/core/Loader_test.php
@@ -31,12 +31,12 @@
 		$this->assertFalse($this->load->is_loaded(ucfirst($lib)));
 
 		// Test loading as an array.
-		$this->assertNull($this->load->library(array($lib)));
+		$this->assertInstanceOf('CI_Loader', $this->load->library(array($lib)));
 		$this->assertTrue(class_exists($class), $class.' does not exist');
 		$this->assertAttributeInstanceOf($class, $lib, $this->ci_obj);
 
 		// Test a string given to params
-		$this->assertNull($this->load->library($lib, ' '));
+		$this->assertInstanceOf('CI_Loader', $this->load->library($lib, ' '));
 
 		// Create library w/o class
 		$lib = 'bad_test_lib';
@@ -47,7 +47,7 @@
 			'RuntimeException',
 			'CI Error: Unable to load the requested class: '.ucfirst($lib)
 		);
-		$this->assertNull($this->load->library($lib));
+		$this->assertInstanceOf('CI_Loader', $this->load->library($lib));
 	}
 
 	// --------------------------------------------------------------------
@@ -63,7 +63,7 @@
 		$this->ci_vfs_create($ext, '<?php class '.$ext.' extends '.$class.' { }', $this->ci_app_root, 'libraries');
 
 		// Test loading with extension
-		$this->assertNull($this->load->library($lib));
+		$this->assertInstanceOf('CI_Loader', $this->load->library($lib));
 		$this->assertTrue(class_exists($class), $class.' does not exist');
 		$this->assertTrue(class_exists($ext), $ext.' does not exist');
 		$this->assertAttributeInstanceOf($class, $name, $this->ci_obj);
@@ -71,13 +71,13 @@
 
 		// Test reloading with object name
 		$obj = 'exttest';
-		$this->assertNull($this->load->library($lib, NULL, $obj));
+		$this->assertInstanceOf('CI_Loader', $this->load->library($lib, NULL, $obj));
 		$this->assertAttributeInstanceOf($class, $obj, $this->ci_obj);
 		$this->assertAttributeInstanceOf($ext, $obj, $this->ci_obj);
 
 		// Test reloading
 		unset($this->ci_obj->$name);
-		$this->assertNull($this->load->library($lib));
+		$this->assertInstanceOf('CI_Loader', $this->load->library($lib));
 		$this->assertObjectNotHasAttribute($name, $this->ci_obj);
 
 		// Create baseless library
@@ -91,7 +91,7 @@
 			'RuntimeException',
 			'CI Error: Unable to load the requested class: '.$lib
 		);
-		$this->assertNull($this->load->library($lib));
+		$this->assertInstanceOf('CI_Loader', $this->load->library($lib));
 	}
 
 	// --------------------------------------------------------------------
@@ -114,7 +114,7 @@
 
 		// Test object name and config
 		$obj = 'testy';
-		$this->assertNull($this->load->library($lib, NULL, $obj));
+		$this->assertInstanceOf('CI_Loader', $this->load->library($lib, NULL, $obj));
 		$this->assertTrue(class_exists($class), $class.' does not exist');
 		$this->assertAttributeInstanceOf($class, $obj, $this->ci_obj);
 		$this->assertEquals($cfg, $this->ci_obj->$obj->config);
@@ -133,7 +133,7 @@
 		$this->ci_vfs_create(ucfirst($lib), '<?php class '.$class.' { }', $this->ci_app_root, 'libraries');
 
 		// Load library
-		$this->assertNull($this->load->library($lib));
+		$this->assertInstanceOf('CI_Loader', $this->load->library($lib));
 
 		// Was the model class instantiated.
 		$this->assertTrue(class_exists($class), $class.' does not exist');
@@ -155,17 +155,17 @@
 		$this->ci_vfs_create(ucfirst($driver), $content, $this->ci_base_root, 'libraries/'.$dir);
 
 		// Test loading as an array.
-		$this->assertNull($this->load->driver(array($driver)));
+		$this->assertInstanceOf('CI_Loader', $this->load->driver(array($driver)));
 		$this->assertTrue(class_exists($class), $class.' does not exist');
 		$this->assertAttributeInstanceOf($class, $driver, $this->ci_obj);
 
 		// Test loading as a library with a name
 		$obj = 'testdrive';
-		$this->assertNull($this->load->library($driver, NULL, $obj));
+		$this->assertInstanceOf('CI_Loader', $this->load->library($driver, NULL, $obj));
 		$this->assertAttributeInstanceOf($class, $obj, $this->ci_obj);
 
 		// Test a string given to params
-		$this->assertNull($this->load->driver($driver, ' '));
+		$this->assertInstanceOf('CI_Loader', $this->load->driver($driver, ' '));
 	}
 
 	// --------------------------------------------------------------------
@@ -180,14 +180,14 @@
 		$this->ci_vfs_create($model, $content, $this->ci_app_root, 'models');
 
 		// Load model
-		$this->assertNull($this->load->model($model));
+		$this->assertInstanceOf('CI_Loader', $this->load->model($model));
 
 		// Was the model class instantiated.
 		$this->assertTrue(class_exists($model));
 		$this->assertObjectHasAttribute($model, $this->ci_obj);
 
 		// Test no model given
-		$this->assertNull($this->load->model(''));
+		$this->assertInstanceOf('CI_Loader', $this->load->model(''));
 	}
 
 	// --------------------------------------------------------------------
@@ -206,7 +206,7 @@
 
 		// Load model
 		$name = 'testors';
-		$this->assertNull($this->load->model($subdir.'/'.$model, $name));
+		$this->assertInstanceOf('CI_Loader', $this->load->model($subdir.'/'.$model, $name));
 
 		// Was the model class instantiated?
 		$this->assertTrue(class_exists($model));
@@ -240,8 +240,8 @@
 
 	// public function testDatabase()
 	// {
-	// 	$this->assertNull($this->load->database());
-	// 	$this->assertNull($this->load->dbutil());
+	// 	$this->assertInstanceOf('CI_Loader', $this->load->database());
+	// 	$this->assertInstanceOf('CI_Loader', $this->load->dbutil());
 	// }
 
 	// --------------------------------------------------------------------
@@ -265,7 +265,7 @@
 		$this->ci_instance_var('output', $output);
 
 		// Test view output
-		$this->assertNull($this->load->view($view, array($var => $value)));
+		$this->assertInstanceOf('CI_Loader', $this->load->view($view, array($var => $value)));
 	}
 
 	// --------------------------------------------------------------------
@@ -311,8 +311,8 @@
 		$val1 = 'bar';
 		$key2 = 'boo';
 		$val2 = 'hoo';
-		$this->assertNull($this->load->vars(array($key1 => $val1)));
-		$this->assertNull($this->load->vars($key2, $val2));
+		$this->assertInstanceOf('CI_Loader', $this->load->vars(array($key1 => $val1)));
+		$this->assertInstanceOf('CI_Loader', $this->load->vars($key2, $val2));
 		$this->assertEquals($val1, $this->load->get_var($key1));
 		$this->assertEquals(array($key1 => $val1, $key2 => $val2), $this->load->get_vars());
 	}
@@ -333,7 +333,7 @@
 		$this->ci_vfs_create($this->prefix.$helper.'_helper', $content, $this->ci_app_root, 'helpers');
 
 		// Load helper
-		$this->assertNull($this->load->helper($helper));
+		$this->assertInstanceOf('CI_Loader', $this->load->helper($helper));
 		$this->assertTrue(function_exists($func), $func.' does not exist');
 		$this->assertTrue(function_exists($exfunc), $exfunc.' does not exist');
 
@@ -378,7 +378,7 @@
 		$this->ci_vfs_create($files, NULL, $this->ci_base_root, 'helpers');
 
 		// Load helpers
-		$this->assertNull($this->load->helpers($helpers));
+		$this->assertInstanceOf('CI_Loader', $this->load->helpers($helpers));
 
 		// Verify helper existence
 		foreach ($funcs as $func) {
@@ -395,7 +395,7 @@
 		$lang = $this->getMock('CI_Lang', array('load'));
 		$lang->expects($this->once())->method('load')->with($file);
 		$this->ci_instance_var('lang', $lang);
-		$this->assertNull($this->load->language($file));
+		$this->assertInstanceOf('CI_Loader', $this->load->language($file));
 	}
 
 	// --------------------------------------------------------------------
@@ -413,24 +413,24 @@
 
 		// Add path and verify
 		$path = APPPATH.$dir.'/';
-		$this->assertNull($this->load->add_package_path($path));
+		$this->assertInstanceOf('CI_Loader', $this->load->add_package_path($path));
 		$this->assertContains($path, $this->load->get_package_paths(TRUE));
 
 		// Test successful load
-		$this->assertNull($this->load->library($lib));
+		$this->assertInstanceOf('CI_Loader', $this->load->library($lib));
 		$this->assertTrue(class_exists($class), $class.' does not exist');
 
 		// Add another path
 		$path2 = APPPATH.'another/';
-		$this->assertNull($this->load->add_package_path($path2));
+		$this->assertInstanceOf('CI_Loader', $this->load->add_package_path($path2));
 		$this->assertContains($path2, $this->load->get_package_paths(TRUE));
 
 		// Remove last path
-		$this->assertNull($this->load->remove_package_path());
+		$this->assertInstanceOf('CI_Loader', $this->load->remove_package_path());
 		$this->assertNotContains($path2, $this->load->get_package_paths(TRUE));
 
 		// Remove path and verify restored paths
-		$this->assertNull($this->load->remove_package_path($path));
+		$this->assertInstanceOf('CI_Loader', $this->load->remove_package_path($path));
 		$this->assertEquals($paths, $this->load->get_package_paths(TRUE));
 
 		// Test failed load without path
diff --git a/user_guide_src/source/changelog.rst b/user_guide_src/source/changelog.rst
index 892bbfb..32d033c 100644
--- a/user_guide_src/source/changelog.rst
+++ b/user_guide_src/source/changelog.rst
@@ -381,6 +381,7 @@
 
    -  :doc:`Loader Library <libraries/loader>` changes include:
 
+      -  Added method chaining support.
       -  Added method ``get_vars()`` to the Loader to retrieve all variables loaded with ``$this->load->vars()``.
       -  ``_ci_autoloader()`` is now a protected method.
       -  Added autoloading of drivers with ``$autoload['drivers']``.