I have a PHP CodeIgniter website that collects volunteer registrations. One thing I have noticed is that the controllers are getting really big. The controllers have many methods, and each method is fairly long. I believe this could be a code smell, and I would like some help and advice on how to refactor the code.
The website is in production and too big to post all the code. I will pick a sample page to post here so you can get an idea. I'm looking for broad feedback on how to improve the organization of the code. For example:
- Idea 1 - Should I move some of this code to a new layer between the controller and the model? I've seen this recommended in some places. I believe it's called a domain layer.
- Idea 2 - Should I break this entire method (and other big methods) off into their own classes? Since I'm using CodeIgniter, I guess these would be library classes?
- Idea 3 - Should I make more use of private methods within the giant controller class? Won't shrink the size of the controller, but might make some of the code more readable. Or maybe not, since I'd be scrolling all over the page to find code, instead of just reading it linearly.
- Idea 4 - Should I try to move more code into the models? My models are a similar size to the controllers.
- Idea 5 - The code works and I (the only developer) can read it just fine. Should I just leave it alone? YAGNI? Keep complexity down?
- I have a lot of controller methods that start out with form validation code. What is the best way to handle that?
- Bonus question: What are good ways to test a website? I currently use iMacros browser plugin and run scripts that simulate typing, mouse clicks, submitting forms, clicking links, etc. I have heard of unit testing but am not sure where to start with that. Is it worth it to write a bunch of unit tests?
- Other ideas/feedback?
Screenshot
Code
public function edit_group($group_id_and_text = NULL)
{
$this->data = set_page_title('Edit Group', $this->data);
$this->data = $this->_get_group_data_or_throw_error_page($group_id_and_text, $this->data, '/managers/edit_group/');
$this->data['list_of_shifts'] = $this->shift_model->get_race_shifts_with_enrolled_plus_groups_fields($this->data['race']['race_id']);
$this->data['list_of_group_shifts'] = $this->group_shift_model->get_group_shifts_for_group($this->data['group']['group_id']);
// output format is $shift_id => $number_promised
$this->data['list_of_group_shifts_as_list'] = $this->group_shift_model->get_group_shifts_as_list($this->data['group']['group_id'], $this->data['race']['race_id']);
$this->data['list_of_shifts_to_highlight'] = highlight_shifts_that_need_more_groups($this->data['list_of_shifts']);
$this->data['list_of_volunteers_with_shifts'] = $this->volunteer_shift_model->get_volunteers_by_race_and_group($this->data['race']['race_id'], $this->data['group']['group_id']);
$this->data['list_of_volunteers_without_shifts'] = $this->volunteer_shift_model->get_volunteers_with_no_shifts_filter_by_group($this->data['race']['race_id'], $this->data['group']['group_id']);
$this->data['list_of_volunteers'] = array_merge($this->data['list_of_volunteers_with_shifts'], $this->data['list_of_volunteers_without_shifts']);
$this->form_validation->set_rules('group_name', 'Group Name', 'trim|required|max_length[200]');
$this->form_validation->set_rules('shift_id[]', 'Volunteer Shift', 'trim|valid_volunteer_group_shift_for_admin[' . $this->data['race']['race_id'] . ']');
$this->form_validation->set_rules('group_notes', 'Group Notes', 'trim|max_length[1000]');
$this->form_validation->set_rules('group_send_reminder_emails', 'Reminder Emails', 'trim|required|in_list[0,1]');
$this->form_validation->set_rules('fix_shifts', 'Fix Volunteer Shifts', 'trim|in_list[1]');
$this->form_validation->set_rules('email_group_leader', 'Email Group Leader', 'trim|in_list[1]');
$this->form_validation->set_rules('delete_group', 'Delete Group', 'trim|in_list[1]');
$this->form_validation->set_rules('delete_group_members', 'Delete Group Members', 'trim|in_list[1]');
$this->form_validation->set_rules('group_firm', 'Mark As Firm', 'trim|in_list[1]');
// Note: SQL WHERE is case insensitive, which is good in this case
$this->data['group_for_duplicate_check'] = $this->group_model->get_group_by_race_and_group_name(
$this->data['race']['race_id'],
$this->input->post('group_name')
);
if ($this->form_validation->run() === FALSE)
{
load_page_with_event_nav('managers/edit_group', $this->data);
}
elseif ( $this->data['group_for_duplicate_check'] && $this->data['group_for_duplicate_check']['group_id'] != $this->data['group']['group_id'] )
{
add_message('error', 'A group with this name already exists. <a href="/managers/edit_group/' . $this->data['group_for_duplicate_check']['group_uri'] . '">Click here</a> to view and edit the existing group.');
load_page_with_event_nav('managers/edit_group', $this->data);
}
else
{
$group_leader_volunteer_id = $this->data['group']['group_leader_volunteer_id'];
// Putting delete_group_members before delete_group so that the soft deleted volunteers keep their group_id. Will be helpful if I have to undo the soft delete.
if ( $this->input->post('delete_group_members') )
{
$volunteer_ids = $this->data['list_of_volunteers'];
if ( $volunteer_ids ) {
$volunteer_ids = sql_make_list_from_sql_result_array($volunteer_ids, 'volunteer_id');
$volunteer_ids = mv_eliminate_duplicates($volunteer_ids);
$this->volunteer_model->soft_delete_volunteer($volunteer_ids);
add_message('success', '"' . html_escape($this->data['group']['group_name']) . '"\'s group members were successfully deleted from the group AND the volunteer database.');
}
}
if ( $this->input->post('delete_group') )
{
$this->group_model->soft_delete_groups($this->data['group']['group_id']);
// If group_leader_volunteer_id got deleted because the volunteer got soft deleted, AND the group is getting deleted, restore the group_leader_volunteer_id so that email_list can display deleted group leaders.
$this->group_model->set_group_leader($this->data['group']['group_id'], $group_leader_volunteer_id);
add_message('success', '"' . html_escape($this->data['group']['group_name']) . '" was successfully deleted.');
}
else
{
$this->group_model->edit_group($this->data);
// refresh some variables needed down here
$this->data['group'] = $this->group_model->get_group_by_id($this->data['group']['group_id']);
$this->data['volunteer'] = $this->volunteer_model->get_volunteer_by_id($this->data['group']['group_leader_volunteer_id']);
$this->data['list_of_group_shifts'] = $this->group_shift_model->get_group_shifts_for_group($this->data['group']['group_id']);
$shifts_to_compare = $this->data['list_of_group_shifts_as_list'];
/*
PHP's array compare is extremely loose.
1) It compares array contents, not references.
2) It compares across types. For example, 0 and '0' are seen as the same thing.
3) The order of the array keys doesn't matter, so we don't need to sort them.
*/
if ( $this->input->post('shift_id') != $shifts_to_compare )
{
$this->group_shift_model->hard_delete_groups_shifts($this->data['group']['group_id']);
foreach ( $_POST['shift_id'] as $shift_id => $value )
{
if ( $value != 0 )
{
$this->group_shift_model->add_shift(
$this->data['group']['group_id'],
$shift_id,
$value,
$this->data
);
}
}
}
// refresh again
$this->data['list_of_group_shifts'] = $this->group_shift_model->get_group_shifts_for_group($this->data['group']['group_id']);
$list_of_group_shifts = sql_make_list_from_sql_result_array($this->data['list_of_group_shifts'], 'shift_id');
// Make sure the group leader is enrolled in all the group's shifts. This is important so that the group leader receives the volunteer instructions for each of this group's shifts.
if ( $this->data['volunteer'] )
{
foreach ( $list_of_group_shifts as $key => $shift_id )
{
$shift = $this->volunteer_shift_model->get_shift_by_volunteer_id_and_shift_id($this->data['volunteer']['volunteer_id'], $shift_id);
if ( ! $shift )
{
$this->volunteer_shift_model->add_shift(
$this->data['volunteer']['volunteer_id'],
$shift_id,
$this->data['auth']['manager']['manager_id']
);
}
}
}
add_message('success', '"<a href="/managers/edit_group/' . $this->data['group']['group_uri'] . '">' . html_escape($this->data['group']['group_name']) . '</a>" was successfully edited.');
if ( $this->input->post('fix_shifts') == 1 )
{
// ****** FIX_SHIFTS_REMOVE ******
foreach ( $this->data['list_of_volunteers_with_shifts'] as $key => $volunteer_shift )
{
if ( !in_array($volunteer_shift['shift_id'], $list_of_group_shifts) )
{
$this->volunteer_shift_model->hard_delete_one_volunteer_one_shift($volunteer_shift['volunteer_id'], $volunteer_shift['shift_id']);
}
}
// ****** FIX_SHIFTS_ADD ******
$this->data['list_of_volunteers_not_joined_with_volunteer_shifts'] = $this->volunteer_model->get_volunteers_by_race_and_group($this->data['race']['race_id'], $this->data['group']['group_id']);
$this->data['list_of_volunteer_shifts_for_this_race'] = $this->volunteer_shift_model->get_volunteers_by_race_order_by_name($this->data['race']['race_id']);
foreach ( $this->data['list_of_volunteers_not_joined_with_volunteer_shifts'] as $key => $volunteer )
{
foreach ( $list_of_group_shifts as $key => $shift_id )
{
$shift_already_exists = sql_search_result_array_contains_key1_value1_key2_value2(
$this->data['list_of_volunteer_shifts_for_this_race'],
'volunteer_id',
$volunteer['volunteer_id'],
'shift_id',
$shift_id
);
if ( ! $shift_already_exists )
{
$this->volunteer_shift_model->add_shift(
$volunteer['volunteer_id'],
$shift_id,
$this->data['auth']['manager']['manager_id']
);
}
}
}
$this->group_shift_model->fix_more_enrolled_vols_than_estimated_vols($this->data['group']['group_id']);
}
if ( $this->input->post('email_group_leader') == 1 )
{
// return true for non-NULL, non-zero
if ( $this->data['group']['group_leader_volunteer_id'] )
{
$this->data['list_of_this_groups_shifts'] = $this->shift_model->get_group_shifts($this->data['group']['group_id']);
send_group_confirmation_email($this->data, $this->data['volunteer']['volunteer_email']);
add_message('success', 'Also, we e-mailed the group leader a group confirmation e-mail.');
}
else
{
add_message('error', 'You requested that we send a confirmation e-mail, but we were unable to because a volunteer group leader was not provided.');
}
}
}
$this->shift_model->recalculate_shift_stats($this->data['race']['race_id']);
redirect_and_die('/managers/group_report/' . $this->data['race']['race_uri']);
}
}