• 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Simple HMVC

#1
Hi everyone!
I would like to share my approach of adding HMVC to CodeIgniter. I call it Simple HMVC

Simple HMVC is an extension for CodeIgniter 3.x, but should also work in 2.x
It makes it possible to use Controller in Controller calls. Direct routing to a HMVC module is not support for one simple reason. I don't believe HMVC should support direct routing.

In my opinion I think HMVC is a collection of small independent applications within a lager MVC application. CodeIgniter is that lager MVC application. Simple HMVC makes it possible to use module based applications by only extending the CodeIgniter core Loader. My approach also keeps the loosely coupled system of CodeIgniter intact. The freedom you have in your regular controllers, libraries, models, views and helpers is extended to Simple HMVC. Systems like Modular Extensions – HMVC by WireDesignz make you use a stricter form of coding for your modules.

How to install Simple HMVC

Step 1
Create a MY_Loader.php file in the directory ./application/core/MY_loader.php If you already have this file edit this file by adding the methods shown in the code below

PHP Code:
<?php  if (! defined('BASEPATH')) exit('No direct script access allowed');

/**
 * My Loader
 *
 * This class extends the Crossfire core Loader. It adds the methods for
 * using Simple HMVC in your CodeIgniter 3.x projects. It also adds 
 * a method for using views stored in your libraries directory
 *
 * @author  Martin Langenberg
 */

class MY_Loader extends CI_Loader {

 
   public function __construct() {
 
       log_message('debug'"Class ".get_class($this)." Initialized.");
 
 
       parent::__construct();
 
   }
 
 
/**
  * Set the view path for a library
  *
  * Prepends a library view path to the view paths array.
  * Enables using views from within a library. This helps keep
  * related files in the same parent directory
  *
  * @param string name of the library
  */
 
 public function library_view($library$view_cascade TRUE) {
 
     $path APPPATH.'libraries/'.$library.'/';
 
     $this->_ci_view_paths = array($path.'views/' => $view_cascade) + $this->_ci_view_paths;
 
 }
 
 
 ###################################################################################
 
 # HMVC MODULE METHODS
 
 ###################################################################################
 
 /**
  * Load module and execute the called method
  * 
  * @param string $module
  * @param string $method
  * @param array $params
  * @return mixed, the output of the called controller/method combination
  */
 
 public function module($module$method NULL$params = array() ) {
 
     // Define the base path to the modules directory
 
     define('MOD_BASE_PATH'APPPATH.'modules/');
 
 
     require_once MOD_BASE_PATH.'base_module.php';
 
 
     $dir NULL;
 
     $dir_path NULL;
 
     $controller NULL;
 
     $controller_path NULL;
 
 
     // Get the CI Super object
 
     $CI =& get_instance();
 
 
     // Load main Controller or sub controller?
 
     $application $this->_load_module($module);
 
 
     if( ! array_key_exists('error'$application) ) {
 
         foreach ($application as $key => $value) {
 
             $$key $value;
 
         }
 
     } else {
 
         return $this->_module_error($module$application);
 
     }
 
 
     // Has the Controller already been loaded? No, then load it
 
     if( ! property_exists($CI$controller) ) {
 
         $this->_ci_module_paths($dir_pathTRUE);
 
         // Include the file
 
         log_message('debug'"The module {$controller} has been loaded.");
 
         require_once $controller_path;
 
 
         // Auto load config file for the loaded controller
 
         $config NULL;
 
         if$config_path set_realpath($dir_path.'/config/'.$controller.'.php'TRUE) ) {
 
             require_once $config_path;
 
         }
 
 
         // Create the new Object of the required module and register as loaded
 
         $object ucfirst($controller).'_Controller';
 
         $app = new $object($config);
 
         $this->_modules[$controller] = $app;
 
 
         // Add a reference to the CI super object easy use later on if needed
 
         $CI->{$controller} = $this->_modules[$controller];
 
    } else {
 
        log_message('debug'"The controller {$controller} of module {$dir} has already been loaded. Second attempt ignored");
 
    }
 
 
    // Execute called method if set
 
    if( ! is_null($method) ) {
 
        // Check if called method exists
 
        if( ! method_exists($app$method) ) {
 
            $application['error_type'] = "Unknow method {$method}()";
 
            $application['error'] = "The method <strong>\"{$method}\"</strong> does not exist in the called controller <strong>\"{$controller}\"</strong> of the requested module <strong>\"{$dir}\"</strong>";
 
            return $this->_module_error($module$application);
 
        }
 
        // Run and return
 
        return $CI->{$controller}->$method($params);
 
    }
 
 
    // Return the controller object;
 
    return $this->_modules[$controller];
 }
 
 
/**
  * Add module paths to the corresponding paths arrays
  *
  * This method only loads the paths of a given module.
  * Makes it possible to use any module resource in a second HMVC module
  * without the overhead of the complete module controller
  * 
  * @param string $module
  */
 
 public function module_paths($module) {
 
     // Get the CI Super object
 
     $CI =& get_instance();
 
 
     // Load main Controller or sub controller?
 
     $application $this->_load_module($module);
 
 
     ifarray_key_exists('error'$application) ) {
 
         log_message('error'"{$application['error_type']}{$application['error']}");
 
     }
 
 
     // Check if module has already been loaded
 
     if( ! property_exists($CI$application['controller']) ) {
 
         $this->_ci_module_paths($application['dir_path'], TRUE);
 
     }
 
 
     return;
 
 }
 
 
/**
  *
  * @param string $module
  * @return string
  */
 
 private function _load_module($module) {
 
     $dir '';
 
 
     // Are we calling a sub controller?
 
     if( ($last_slash strrpos($module'/') ) !== FALSE) {
 
         // The dir is in front of the last slash
 
         $dir str_replace('/'''substr($module0$last_slash 1) );
 
 
         // And the module name behind it
 
         $controller substr($module$last_slash 1);
 
     } else {
 
         $dir $module;
 
         $controller $module;
 
     }
 
 
     if$dir_path set_realpath(MOD_BASE_PATH.$dirTRUE) ) {
 
         if($controller_path set_realpath(MOD_BASE_PATH.$dir.'/controllers/'.$controller.'.php'TRUE) ) {
 
             return array('dir'=>$dir,'dir_path'=>$dir_path,'controller'=>$controller'controller_path'=>$controller_path);
 
         } else {
 
             $error_type "Unknow controller {$controller}";
 
             $error "The controller <strong>\"{$controller}\"</strong> for the requested module <strong>\"{$dir}\"</strong> does not exist";
 
         }
 
     } else {
 
         $error_type "Unknow controller {$dir}";
 
         $error "The requested module <strong>\"{$dir}\"</strong> does not exist";
 
     }
 
 
     return array('error_type'=>$error_type,'error'=>$error,'dir'=>$dir,'controller'=>$controller);
 
 }
 
 
/**
  * Add module paths to the corresponding paths arrays
  *
  * This method adds the available paths for a requested HMVC module to the already set
  * paths array used for models, libraries, helpers, views and configs
  * 
  * @param string $path
  * @param boolean $view_cascade
  */
 
 private function _ci_module_paths($path$view_cascade TRUE) {
 
     if(set_realpath($path.'libraries'TRUE) ) {
 
         array_unshift($this->_ci_library_paths$path);
 
     }
 
     if(set_realpath($path.'models'TRUE) ) {
 
          array_unshift($this->_ci_model_paths$path);
 
     }
 
     if(set_realpath($path.'helpers'TRUE) ) {
 
         array_unshift($this->_ci_helper_paths$path);
 
     }
 
     if($viewpath set_realpath($path.'view'TRUE) ) {
 
         // Add the path for loading views
 
         $this->_ci_view_paths = array($viewpath => $view_cascade) + $this->_ci_view_paths;
 
     }
 
 
     // Add config file path
 
     if($viewpath set_realpath($path.'config'TRUE) ) {
 
         $config =& $this->_ci_get_component('config');
 
         array_unshift($config->_config_paths$path);
 
     }
 
 }
 
 
/**
  * 
  * @param string $module
  * @param array $application
  * @return string
  */
 
 private function _module_error($module$application) {
 
     ob_start();
 
     $this->view('errors/html/error_module', array('module'=>$module,'application'=>$application) );
 
     $buffer ob_get_clean();
 
     return $buffer;
 
 }
 


Step 2
Create a directory named modules in your CodeIgniter application directory → ./application/modules

Step 3
Create the file Base_module.php in the directory ./application/modules/Base_module.php. Place the code shown below in this file. Keep adding shared code for modules if required to this file
PHP Code:
<?php  if (! defined('BASEPATH')) exit('No direct script access allowed');

/**
 * Simple HMVC Base module
 * 
 * This class is the base for Simple HMVC modules. It is auto loaded when requesting a 
 * Simple HMVC module. Extend your own module controller from this base the have access
 * to the base functions that you add your self
 *
 * @author  Martin Langenberg
 */

class Base_module {
 
 
 public function __construct() {
 
 
     log_message('debug'"Class ".get_class($this)." Initialized.");
 
 
 }
 
 
/**
  * __get
  *
  * Enables the use of CI super-global without having to define an extra variable.
  *
  * @access public
  * @param $var
  * @return mixed
  */
 
 public function __get($var) {
 
     return get_instance()->$var;
 
 }
 


Step 4
Create a module directory structure as follows in ./application/modules/:
  • your_module – The name of your new module
    • config
    • controllers
    • helpers
    • libraries
    • models
    • views
Step 5
Create a controller with the same name as your directory your_module in the controllers directory of the module. You can also add sub controllers. Extend controllers from the Base_module class if you wish to use the shared code in your controller

Step 6
Build your module application as you would build a small CodeIgniter project!

Error display
Create a view file in ./application/views/errors/html/error_module.php and add the following to it
Code:
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
?>

<style type="text/css">

::selection { background-color: #E13300; color: white; }
::-moz-selection { background-color: #E13300; color: white; }

body {
background-color: #fff;
margin: 40px;
font: 13px/20px normal Helvetica, Arial, sans-serif;
color: #4F5155;
}

a {
color: #003399;
background-color: transparent;
font-weight: normal;
}

h2 {
color: #444;
background-color: transparent;
border-bottom: 1px solid #D0D0D0;
font-size: 19px;
font-weight: normal;
margin: 0 15px 15px;
padding: 15px 0px;
}

code {
font-family: Consolas, Monaco, Courier New, Courier, monospace;
font-size: 12px;
background-color: #f9f9f9;
border: 1px solid #D0D0D0;
color: #002166;
display: block;
margin: 14px 0 14px 0;
padding: 12px 10px 12px 10px;
}

#container {
margin: 10px;
border: 1px solid #D0D0D0;
box-shadow: 0 0 8px #D0D0D0;
}

p {
margin: 12px 15px 12px 15px;
}
</style>

<div id="container">
<h2>Module error: <?php echo $application['error_type']; ?></h2>

<p>
<?php echo $application['error']; ?>
</p>
</div>

How to use Simple HMVC

From anywhere within CodeIgniter you can load a module like this:
PHP Code:
$controller $this->load->module('your_module'); 

This will load a controller named your_module within the module your_module. This will return the controller object.

PHP Code:
$controller $this->load->module('your_module/sub_controller'); 

This will load a controller named sub_controller within the module your_module. This will return the controller object.

PHP Code:
$output $this->load->module('your_module''index'); 

This will load a controller named your_module within the module your_module and call the method index within the controller. The output of the method index will be returned.

PHP Code:
$params = array();
$params['arg1'] = 1;
$params['arg2'] = 2;
$params['arg3'] = 3;
$output $this->load->module('your_module''index'$params); 

The same call, but now added params are passed to the index method of the called module controller. The params must be an array. How your controller futher handles it is up to you.

Once loaded a module can also be accessed via it's CI super object alias like so:

PHP Code:
$this->your_controller->some_method(); 

Module resources can be reused by another module without the need of loading the complete module. You can do this by loading the paths only

PHP Code:
$this->load->module_paths('some_other_module'); 

Using config files
In the config directory of the module you can add config files for controllers and libraries. Just use the same filename for both controllers and libraries so that config files are auto loaded. Remember to add $config = null to the contructors of the classes

All required files have been added as an attachment. I would like to hear what the CI community thinks of my approach

EDIT: Have added MIT License


Attached Files
.zip   simple_hmvc.zip (Size: 13.82 KB / Downloads: 393)
.txt   license.txt (Size: 1.06 KB / Downloads: 175)
Reply

#2
I have updated the attached zip file
Reply

#3
This looks pretty cool. How would you autoload a module or module component?
Reply

#4
I don't have an extended autoload method, and for my own use I won't need one. But what you can do is create a module_loader.php in ./application/libraries and autoload that. Within this module autoloader you can then load any module or module components you want.

I will add an example later!
Reply

#5
(09-24-2015, 01:35 PM)skunkbad Wrote: This looks pretty cool. How would you autoload a module or module component?

I have added an example Module_loader and have updated the zip file attached to the original post.

The Module_loader.php
PHP Code:
<?php  if (! defined('BASEPATH')) exit('No direct script access allowed');

/**
 * Simple HMVC Autoloader
 * 
 * Simple HMVC does not extend the core autoload function. To autoload modules or any module components
 * you can load them in this library and have CI auto load this library. 
 * It's a simple workaround and like Simple HMVC does not replace or effect core CI code
 * 
 * NOTE: When auto loading, only load the module as an object so it is available through 
 * the CI super object as $this->your_module->some_method(); Calling a method through this
 * library will only complicate your code ;)
 *
 * @author  Martin Langenberg
 */

class Module_loader {
 
 
   // Set to TRUE to autoload based on route
 
   private $load_by_route TRUE;
 
 
   /* The CI uri segments
     * You can use this to base auto loading on a specific route
     */
 
   private $segments = array();
 
 
   public function __construct($config NULL) {
 
 
       log_message('debug'"Class ".get_class($this)." Initialized.");
 
 
       if($this->load_by_route) {
 
           $this->segments $this->uri->segments;
 
           // No segments available, set as home
 
           if( ! array_key_exists(1$this->segments) ) {
 
               $this->segments[1] = 'home';
 
           }
 
       }
 
 
       $this->_autoload();
 
   }
 
 
   /**
     * __get
     *
     * Enables the use of CI super-global without having to define an extra variable.
     *
     * @access public
     * @param $var
     * @return mixed
     */
 
   public function __get($var) {
 
       return get_instance()->$var;
 
   }
 
 
   /**
     * _autoload
     * 
     * Auto loads any module or module components you want
     */
 
   private function _autoload() {
 
 
       if($this->load_by_route) {
 
 
           // Load these modules based on route
 
           switch $this->segments[1] ) {
 
               case 'home':
 
                   // Load a module main controller object
 
                   $this->load->module('your_module');
 
 
                   // Load a module sub controller object
 
                   $this->load->module('your_module/sub_controller');
 
 
                   // Load module component(s)
 
                   $this->load->module_paths('some_module');
 
                   // Load the components you want like loading any available CI resource
 
                   $this->load->library('some_module_library');
 
                   $this->load->helper('some_module_helper');
 
                   $this->load->model('som_module_model');
 
 
                   break;
 
           }
 
       }
 
 
       // Always load these modules
 
   }
 

Reply

#6
Very interesting. But the license of your code is unknown.
Would you make it clear?
I hope MIT or BSD license.
Do you want to write tests for your apps? Our book, CodeIgniter Testing Guide would help you.
Reply

#7
It is under MIT license. Will add it to the original post
Reply

#8
I may have missed it somewhere, but don't you need to load the path helper before you can use set_realpath() in your loader?
Reply

#9
(09-25-2015, 06:54 AM)mwhitney Wrote: I may have missed it somewhere, but don't you need to load the path helper before you can use set_realpath() in your loader?

Yes, you do and I auto load it. And I have replaced it with my own version of set_realpath() in a MY_path_helper.php file. The file is included in zip file. I should of mentioned it in the beginning.
Reply

#10
I'm confused. Other than the terminology of the entry class being a "controller" instead of a "library", how is this different than using Packages in CodeIgniter?

If you can't route to it then it seems to be basically a package, which exists in CI and allows for any of the standard CI file types except the controller. The only difference that I can see would be that you don't have to grab the CI instance with get_instance() since you're extending from the CI_Controller?
Support Development  • Practical CodeIgniter 3  •
Myth:AuthVulcan - CLI Tools for CI4
Reply


Digg   Delicious   Reddit   Facebook   Twitter   StumbleUpon  


  Theme © 2014 iAndrew  
Powered By MyBB, © 2002-2020 MyBB Group.