Cleaning Up Your CodeIgniter Controllers

One of the issues I’ve always had with CodeIgniter is that it’s kind of difficult to keep my controllers skinny and my models fat. Certain things that are typically handled in the model with other MVC frameworks are often handled in the controller in CI.

For example, it’s fairly common to see code like this in a controller:

application/controllers/products.php

class Products extends CI_Controller {

    public function edit($product_id)
    {
        $this->form_validation
            ->set_rules('name', 'Name', 'required')
            ->set_rules('price', 'Price', 'required|decimal')
            ->set_rules('short_description', 'Short Description', 'required')
            ->set_rules('long_description', 'Long Description', 'required');

        if ($this->form_validation->run())
        {
            $attributes = array(
                'id' => $product_id,
                'name' => $this->input->post('name'),
                'price' => $this->input->post('price'),
                'short_description' => $this->input->post('short_description'),
                'long_description' => $this->input->post('long_description')
            );

            if ($this->company_model->update($attributes))
            {
                $this->messages->success('Product updated successfully!');
                redirect('products');
            }
            else
            {
                $this->messages->error("There was an error updating this product's attributes...");
            }
        }

        $product = $this->product_model->find($product_id);
        $company = $this->company_model->find($company_id);

        $view_data = array(
            'company' => $company,
            'product' => $company_settings
        );
        $this->load->view('products/edit', $view_data);
    }
}

and a model like this:

application/models/product_model.php

class Product_model extends CI_Model {

    public function find($product_id)
    {
        return $this->db
            ->where('id', $product_id)
            ->limit(1)
            ->get('products')
            ->row();
    }

    public function update($product_id, $attributes)
    {
        return $this->db
            ->where('id', $product_id)
            ->update('products', $attributes);
    }

    public function get_product_price_breaks($product_id)
    {
        return $this->db
            ->where('product_id', $product_id)
            ->get('product_price_breaks');
    }
}

Room for Improvement

At first glance this controller may not look too bad, and if you’re used to working with CodeIgniter you’ve probably seen something close to this pattern a lot. You may think that, “we are separating our code well and we aren’t running queries in our controller or anything so we’re good to go!”, but I think this is a common misconception in the CI community and that there are a few things we can do here to clean up our controllers and fatten up our models.

Move Validation Rules

In MVC frameworks that use an ORM you can setup validations for your data in the model, which helps keep controllers skinny. Since CodeIgniter doesn’t come with an ORM and the form validation class is all we have, it’s a good idea to make use of the ability to save validation rules to a config file.

application/config/form_validation.php

$config = array(
    array(
        'field' => 'name',
        'label' => 'Name',
        'rules' => 'required'
    ),
    array(
        'field' => 'price',
        'label' => 'Price',
        'rules' => 'required|decimal'
    ),
    array(
        'field' => 'short_description',
        'label' => 'Short Description',
        'rules' => 'required'
    ),
    array(
        'field' => 'long_description',
        'label' => 'Long Description',
        'rules' => 'required'
    )
);

Although your config file can get a little long when you are working on a large application (not so much a problem if you are using Modular Extensions and can have module-level config files), it’s still better than fattening your controllers with validation rules all over.

Prep Form Data in the Model

Once we’ve validated our form in the controller and are ready to submit the data to the database, we may as well setup the associative array we need for the update inside the model.

public function update($product_id, $input)
{
    $attributes = array(
        'name' => $input['name'],
        'price' => $input['price'],
        'short_description' => $input['short_description'],
        'long_description' => $input['long_description']
    );

    return $this->db
        ->where('id', $product_id)
        ->update('products', $attributes);
}

Assign Secondary Queries to Object Attributes

One last optimization we can do is perform all of our queries for the view data and return them as a single object. This way we cut down on clutter in the controller and provide a more logical placeholder for the view data. In this case, we’ll make a product’s price breaks available via the price_breaks attribute ($product->price_breaks). We’ll also remove a couple lines of assigning the results from the model to the $view_data variable.

application/models/product_model.php

public function get_all_product_info($product_id)
{
    $product = $this->find($product_id);
    $product->price_breaks = $this->get_product_price_breaks($product_id);

    return $product;
}

application/controllers/products.php

$view_data['product'] = $this->product_model->get_all_product_info($product_id);

Summary

With just a few simple changes we’ve drastically simplified the controller and made it responsible for only what it needs to be. Take a look at our refactored code compared to our original code and look how much cleaner it is.

Refactored

application/controllers/products.php

class Products extends CI_Controller {

    public function edit($product_id)
    {
        if ($this->form_validation->run())
        {
            if ($this->product_model->update($product_id, $this->input->post()))
            {
                $this->messages->success('Product updated successfully!');
                redirect('products');
            }
            else
            {
                $this->messages->error('There was an error updating this product's attributes...');
            }
        }

        $view_data['product'] = $this->product_model->find($product_id);

        $this->load->view('products/edit', $view_data);
    }
}

application/models/product_model.php

class Product_model extends CI_Model {

    public function find($product_id)
    {
        return $this->db
            ->where('id', $product_id)
            ->limit(1)
            ->get('products')
            ->row();
    }

    public function get_all_product_info($product_id)
    {
        $product = $this->find($product_id);
        $product->price_breaks = $this->get_product_price_breaks($product_id);

        return $product;
    }

    public function update($product_id, $input)
    {
        $attributes = array(
            'name' => $input['name'],
            'price' => $input['price'],
            'short_description' => $input['short_description'],
            'long_description' => $input['long_description']
        );

        return $this->db
            ->where('id', $product_id)
            ->update('products', $attributes);
    }

    public function get_product_price_breaks($product_id)
    {
        return $this->db
            ->where('product_id', $product_id)
            ->get('product_price_breaks');
    }
}

application/config/form_validation.php

$config = array(
    array(
        'field' => 'name',
        'label' => 'Name',
        'rules' => 'required'
    ),
    array(
        'field' => 'price',
        'label' => 'Price',
        'rules' => 'required|decimal'
    ),
    array(
        'field' => 'short_description',
        'label' => 'Short Description',
        'rules' => 'required'
    ),
    array(
        'field' => 'long_description',
        'label' => 'Long Description',
        'rules' => 'required'
    )
);

Note: Another very significant benefit to putting more code in the models is that your code will be much easier to test. Models are easy to test because they almost always return something while controllers are difficult because they usually just display a view. Breaking your code up this way will make it a lot easier if you decide to provide test coverage for your application.

Initial Code

application/controllers/products.php

class Products extends CI_Controller {

    public function edit($product_id)
    {
        $this->form_validation
            ->set_rules('name', 'Name', 'required')
            ->set_rules('price', 'Price', 'required|decimal')
            ->set_rules('short_description', 'Short Description', 'required')
            ->set_rules('long_description', 'Long Description', 'required');

        if ($this->form_validation->run())
        {
            $attributes = array(
                'id' => $product_id,
                'name' => $this->input->post('name'),
                'price' => $this->input->post('price'),
                'short_description' => $this->input->post('short_description'),
                'long_description' => $this->input->post('long_description')
            );

            if ($this->company_model->update($attributes))
            {
                $this->messages->success('Product updated successfully!');
                redirect('products');
            }
            else
            {
                $this->messages->error('There was an error updating this product's attributes...');
            }
        }

        $product = $this->product_model->find($product_id);
        $company = $this->company_model->find($company_id);

        $view_data = array(
            'company' => $company,
            'product' => $company_settings
        );
        $this->load->view('products/edit', $view_data);
    }
}

application/models/product_model.php

class Product_model extends CI_Model {

    public function find($product_id)
    {
        return $this->db
            ->where('id', $product_id)
            ->limit(1)
            ->get('products')
            ->row();
    }

    public function update($product_id, $attributes)
    {
        return $this->db
            ->where('id', $product_id)
            ->update('products', $attributes);
    }

    public function get_product_price_breaks($product_id)
    {
        return $this->db
            ->where('product_id', $product_id)
            ->get('product_price_breaks');
    }
}