Welcome Guest, Not a member yet? Register   Sign In
Trying to extend Loader to call app controllers
#1

[eluser]Stuart Marsh[/eluser]
Hi everyone.
I'm fairly new to CodeIgniter but I am already impressed by how flexible and easy it is.
However like many people on here I am disappointed that I cannot call controllers within controllers. I know it goes against the MVC structure and that helpers can be created but this would be a much more flexible way to do things.
So I have started to play with CodeIgniter and I'm trying to extend the Loader library so I can call other controllers. I have a library in my app called MY_Loader.php and this is what I have put in it:
Code:
class MY_Loader extends CI_Loader {
    
    /**
     * Constructor
     *
     * @access    public
     */
    function MY_Loader()
    {
        parent::CI_Loader();
    }
    
    /**
     * Controller Class Loader
     *
     * This function lets users load and instantiate controller classes.
     * It is designed to be called from a user's app controllers.
     *
     * @access    public
     * @param    string    the name of the class
     * @param    mixed    the optional parameters
     * @return    void
     */    
    function Controller($library = '', $params = NULL)
    {        
        if ($library == '')
        {
            return FALSE;
        }

        if (is_array($library))
        {
            foreach ($library as $class)
            {
                $this->_ci_load_controller_class($class, $params);
            }
        }
        else
        {
            $this->_ci_load_controller_class($library, $params);
        }
        
        $this->_ci_assign_to_models();
    }

    // --------------------------------------------------------------------
    
    /**
     * Load Controller class
     *
     * This function loads the requested controller class.
     *
     * @access    private
     * @param     string    the item that is being loaded
     * @param    mixed    any additional parameters
     * @return     void
     */
    function _ci_load_controller_class($class, $params = NULL)
    {    
        // Get the class name
        $class = str_replace(EXT, '', $class);

        // We'll test for both lowercase and capitalized versions of the file name
        foreach (array(ucfirst($class), strtolower($class)) as $class)
        {            
            // Lets search for the requested library file and load it.
            $is_duplicate = FALSE;
            for ($i = 1; $i < 3; $i++)
            {
                $path = ($i % 2) ? APPPATH : BASEPATH;    
                $fp = APPPATH.'controllers/'.$class.EXT;
                
                // Does the file exist?  No?  Bummer...
                if ( ! file_exists($fp))
                {
                    continue;
                }
                
                // Safety:  Was the class already loaded by a previous call?
                if (in_array($fp, $this->_ci_classes))
                {
                    $is_duplicate = TRUE;
                    log_message('debug', $class." class already loaded. Second attempt ignored.");
                    return;
                }
                
                include($fp);
                $this->_ci_classes[] = $fp;
                return $this->_ci_init_class($class, '', $params);
            }
        } // END FOREACH
        
        // If we got this far we were unable to find the requested class.
        // We do not issue errors if the load call failed due to a duplicate request
        if ($is_duplicate == FALSE)
        {
            log_message('error', "Unable to load the requested class: ".$class);
            show_error("Unable to load the requested class: ".$class);
        }
    }
Basically it is a copy of Library and _ci_load_class but with the controller directory. This means controllers I can call the controller with $this->load->Controller('Controller_filename') then $this->controller_name->function(). However, when these 'sub' controllers try to use models or helpers they fail.
For example, If the controller I access uses a model I get the following message.
Code:
A PHP Error was encountered

Severity: Notice

Message: Undefined property: Form_Model::$db

Filename: models/form_model.php

Line Number: 21

Could anybody help me get this working or help me understand why it is failing?
#2

[eluser]thunder uk[/eluser]
I know this doesn't answer your question (sorry!) but honestly, there is no good reason (that I'm aware of) for a controller to expose any of its functionality to another part of your applicaton. If there are functions or methods that are reusable, you should take them out of the controller and place them into models, libraries or helpers (depending upon their nature).

The controller should just be acting as traffic cop, calling methods on models and libraries etc to collect the data for the view it's going to serve. If you start asking him to perform roadside vehicle repairs as well, there's going to be an accident! Wink
#3

[eluser]Stuart Marsh[/eluser]
Let me explain what I'm trying to do and maybe you could suggest a better way.
I want to create a CMS style front-end for my application.
At the moment I have several controllers that deal with specific areas, i.e. static content, navigation menu, order form, product catalog, guestbook.
The front-end will pull together these controllers into one template view.
My front-end controller will contain code like so:
Code:
function index($class, $function)
{
   //Nav menu
   $this->load->Controller('nav');
   //Our content
   $this->load->Controller($class);

   $output['navigation'] = $this->nav->index();
   $output['content'] = $this->$class->$function();

   //Render final view
   $this->load->view('template', $output);
}
The URL will look like this http://www.site.com/index.php/frontend/content/view/2 or http://www.site.com/index.php/frontend/c.../category1.
#4

[eluser]ztinger[/eluser]
Your navigation doesn't need to be a controller, it can be a model.
#5

[eluser]thunder uk[/eluser]
If you want to use just a single frontend controller, you can add a 'catchall' route to /system/application/config/routes.php

Code:
$route[':any'] = "frontend";

If you need any particular request to not go via the frontend controller, simply add the appropriate routes above the catchall.

Then in your frontend controller, you include a _remap function which catches all requests, considers the uri being asked for and calls the necessary methods, loads the appropriate models and libraries to collect the data for the view and then calls the appropriate view to render the output

Code:
function _remap()
{

    // Determine what is being requested. eg content / catalog
    $app_region = $this->uri->segment(1);
    $app_method = $this->uri->segment(2);

    //construct and test for a valid method to call
    // Show the homepage if not valid (or some other way to handle the problem)

    $frontend_method = "_frontend_" . $app_region;

    if(method_exists($frontend_method,$this))
    {
        $output['content'] = $this->$frontend_method($app_method);
    } else {
        $output['content'] = $this->_homepage_content();
    }


    // Get the html for the navigation
    $output['navigation'] = $this->_get_navigation();


    //Render final view
    $this->load->view('template', $output);    

}

function _frontend_catalog($method)
{

    // load the catalog model
    $this->load->model('catalog');


    // Here, you've been passed the method requested, handle accordingly

    switch($method)
    {
         case "list":

             // Get the relevant data from the model
             $data = $this->catalog->getData();
    
             // Request a view to turn the data into html
             $returnValue = $this->load->view('catalog/list',$data,true);

             break;
         case 2 ...
         case 3 ...
         case z ...
    }
            
    // Return the generated HTML
    return $returnValue;

}

function _get_navigation()
{

   // This function could load a model or a library
   // and call a method, passing in params as appropriate
   // and use the returned data to generate the nav, via a view

   // If your nav is pretty static or uses javascript to handle
   // the dynamic portions, it could just call a view

   return $navigation_html;

}
#6

[eluser]Stuart Marsh[/eluser]
True but I may want to call a controller that I don't wont to be in a model.
#7

[eluser]thunder uk[/eluser]
Sorry, but I really can't think why.

A controller simply doesn't expose functionality to other parts of the application. If there's a common function needed by several different parts of your app, it ought to go into a library or a model (or a helper if that's appropriate).
#8

[eluser]Iksander[/eluser]
Use libraries for that stuff.

I use a library that contains all my Authentication code (a 'login' method, 'registration method, and lost password method) and simply load it into my controller. The library handles loading views - I use a splinter view setup so I can pass the container view and the shard view to the library through the parameters in the method being called.

Code:
class Home_controller extends Controller
{
    function Home_controller()
    {
        parent::Controller();
        $this->load->library('sentinel'); // Sentinel is the name of my Authentication system
    }
    function index()
    {
        // Display your home page stuff here.
    }
    function login()
    {
        /**
         * Right here everything is taken care of - it loads the form into the specified template
         * and the form will post back to this controller/method where the library grabs from post array
         * authenticates and redirects to a specified location set in a config file.
         */
        $this->sentinel->login('path/to/login/form', 'path/to/view/template');
    }
    function register()
    {
        /**
         * Same thing as above, you could easily set the view template in the constructor
         * and pass it as a parameter - $this->template
         * This system is very abstract, it generally takes minutes for me to setup and
         * it uses OBSession.
         */
        $this->sentinel->register('path/to/register/form', 'path/to/view/template');
    }
}

Like I said it uses OBSession - but, it is a modified version, I modified OBSession for PHP5 and my library is not backwards compatible with PHP4.

Hope it gives you some ideas though, really, controllers in my opinion should not be aware of other controllers.
#9

[eluser]ztinger[/eluser]
Very interesting approach Iksander. I'd like to know more. Is that Sentinel the same library you have for download?
#10

[eluser]Iksander[/eluser]
Unfortunately no, no downloads.

I have a highly developed version that I use at work (they allowed me to keep the GPL license on code I develop there for it as well, since I started it before I was hired there), but would need quite a bit of cleaning up to be 'releasable'.

It developed some pretty cool helper functions for doing user checks too:
Code:
class Admin_controller
{
    public function __construct()
    {
        parent::Controller();
        $this->load->helper('sentinel_helper');
        // Check for user permissions
        if(!check('admin', '>='))
            redirect('login', 'location');
        // Do whatever else.
    }
}

The check helper function has four conditions to test against the provided role ('user', 'editor', 'admin', 'suser'); greater than or equal to ">=" $role, less than or equal to "<=" $role, equal to "==" $role, not equal to "!=" $role.

The registration system works as well and I have been planning on implementing a more interesting CAPTCHA mechanism (should be very cool).

If I get around to it, I will clean it up and try to do an Alpha release for those that would be willing to twiddle with it.




Theme © iAndrew 2016 - Forum software by © MyBB