views:

4509

answers:

7

I have a function in my controller that has grown longer than I'd prefer and I'd like to refactor it to call a few discrete functions to make it easier to manage. How can I better organize a long function in a Codeigniter controller?

What I've tried:

I know you can create private functions in a controller by naming them with a leading underscore (_myfunc), but then the variables in the function are out of scope for the calling controller function. So you have to return all the needed data from the function which is a hassle.

Is this the best option for managing a complex controller function? Is there an easier way where the variables could all be global to the controller class like a standard class member variable?

Suggestions? Thanks in advance!

EDIT: Someone requested the code so I added code for giant controller below. One opportunity for improvement is to move logic in switch statements to separate functions (delete, preview, order, etc). But I'm trying to decide on the next step after that. Moving the big validation setup code into it's own function would really take some weight out, but where should I move it to?

    function categories() {
 $this->load->library('upload');
 $this->load->model('categories_m');
 $this->load->model('products_m');
 $this->load->model('pages_m');
 $this->load->model('backoffice/backofficecategories_m');
 $data['body'] = $this->load->view('backoffice/categories/navigation_v', '', TRUE);
 $data['cat_tree'] = $this->categories_m->getCategoryTree();
 $data['page_list'] = $this->pages_m->getPageList();
 $data['category_dropdown'] = $this->load->view('backoffice/categories/category_dropdown_v',$data,TRUE);

 switch ($this->uri->segment(3)) { //display views based on parameter in URL.
 case 'delete':   
  $categoryTreeID = $this->sitewide_m->checkURLParam($this->uri->segment(4),'CategoryTree'); //if parameter is in URL, show 404 if invalid parameter is passed. Otherwise, set variable known to be safe.
  if (isset($_POST['delete'])) {
   $this->backofficecategories_m->deleteCategory($categoryTreeID);
   $data['body'] .= '<span class="error">Category Deleted.</span>';
  } else {
   $data['cat_details'] = $this->categories_m->getCategoryDetails('',$categoryTreeID);
   $data['parent_category'] = $this->categories_m->getParentCategory($categoryTreeID);
   $data['products_to_reassign'] = $this->products_m->getProductsInCategory('',$categoryTreeID); 
   $data['body'] .= $this->load->view('backoffice/categories/delete_v',$data,TRUE);  //pull fresh category tree data since tree was just updated.
  }
  break;
 case 'preview':
  if ($this->uri->segment(4)) $data['categoryTreeID'] = $this->sitewide_m->checkURLParam($this->uri->segment(4),'CategoryTree'); //if parameter is in URL, show 404 if invalid parameter is passed. Otherwise, set variable known to be safe.
  $data['cat_details'] = $this->categories_m->getCategoryDetails(NULL,$data['categoryTreeID']); //get category ID being edited from the URL and store it. Returns false if category ID isn't found.
  foreach ($data['cat_details']->result() as $detail) {
   $data['categoryName'] = $detail->Name;
   $data['categoryID'] = $detail->ID;
  }
  $data['body'] .= $this->load->view('backoffice/categories/preview_v', $data, TRUE);
  break;

 ...cases continue...
 default:
  $this->load->library('table');
  $data['body'] .= $this->load->view('backoffice/categories/categories_v', $data, TRUE);
  break;
 }
 $this->load->view('backoffice/template_v',$data);  
}
A: 

What version of PHP are you using? PHP 5 has support for real OO, so you can declare private function that will be treated as such by the interpreter:

private function foo(){
...
}

If you want classes that extend your class (child classes) to have access to the function, replace private with protected.

I've never used CodeIgnniter so I'm afraid I can't help you with your specific problem domain. However, refactoring a function that's grown to long is a very common problem, with common solutions. Martin Fowler is a smart guy who's written some books on the topic that are well-regarded, so you might see if you can find one of his books. There are also tutorials online to get you started refactoring.

Jeremy DeGroot
Thanks for the answer Jeremy. I'm familiar with general refactoring principles, just trying to figure out the best approach when working in the constraints of Codeigniter.
Cory House
+4  A: 

Are you using models? Code igniter doesn't enforce this, but using models in addition to controllers and views is a good way to have a shorter controller function. Alternatively, you could place some of the functions in your own helper, then import it.

And if you want to set some default values for the entire constructor, you can use the class constructor. This is outlined here:

http://codeigniter.com/user_guide/general/controllers.html#constructors

Thanks Stuart. Yup, using models, though I don't care for Codeigniter's retstrictive view of Models. Codeigniter's documentation basically says you only put db queries in models. I'd like to move a lot of my logic to my model but this seems to go against the codeigniter idea that model=db. Agreed?
Cory House
Yes, it's intended that models are just an extra layer for dealing with data, as usual in the MVC approach. If you want to stick to the code igniter "rules", best best is to use the private functions. Or if they're going to be reusable functions, write them in a helper.
+2  A: 

A Service Layer would help.

Adrian Grigore
I believe I understand the gist of a service layer, but could you clarify a bit with an example?
Cory House
If I remember correctly, http://tudu.sourceforge.net/ had a decent Service Layer implementation. Take a peek inside the sources. Here's another pretty simple explanation: http://is.gd/jgl6. Also, check out this piece of code http://is.gd/jgnQ. Good luck!
Adrian Grigore
+1  A: 

If you want to keep your logic in the same controller, you can simulate a private method by placing an underscore before the function name, eg: _myMethod(). Like the link says, an underscore before the function name, prevents CI from calling it from the URL. You could, for instance, create _delete(), _preview(), _order() etc. methods in the Categories controller. If, however, you use the same logic to delete, preview, order etc. other stuff, maybe you should move these methods in a model or helper.

Ch4m3l3on
+2  A: 

Looking at your code, you are using one method for several actions. I would make each action its own method. Common resources could be class members and loaded in the constructor.

So instead of a url like "your_controller/categories/add" you could change your url to "category_controller/add" and have a method for each action. If you don't want to change your urls, then use a route:

$route['your_controller/categories/(.*)'] = 'your_controller/$1';
rick
+1  A: 

Personally, I think you're doing too much with a single controller method. One of the first things I would do is separate your CRUD (Create Read Update Delete) functions as separate methods. For example, your example is for working with "categories", why not have a separate "categories" controller?

class Categories extends Controller
{
  function __construct()
  {
    parent::Controller();
  }

  function index() 
  {
    //display logic/code here
  }

  function edit()
  {
    //get the category to update from the post or url for editing
    //do the editing, etc
  }

  function delete()
  {
    //delete the category
  }

  function add()
  {
    //create the new category
  }
}

Your URLs would reference the categories controller:

http://www.example.com/categories/edit http://www.example.com/categories/delete etc.

The second thing I'd suggest is upgrading to CodeIgniter 1.7.1 - the updated form_validation library makes it easy to move all of your validation rules to a separate config file.

JayTee
Thanks for the post JayTee. I agree both ideas are a step in the right direction. This was the first controller I built in codeigniter and I was still learning best practices within its constraints. It slowly grew too large.
Cory House
A: 

Try looking into the _remap() function of the controller in codeigniter.

Using it you can keep the common code in the _remap function, and then call any other function from within _remap for delete, update etc (based on the uri_segment(3)).

Samnan