Advanced feedback form

Overview

In this example we're going to create a feedback form that allows you to create custom input fields for your users to fill in. Our plugin name will be "AdvancedFeedback" which we will use throughout the plugin files. You may download plugin files before you begin or follow the guide below to create them manually.

Plugin core files

To begin, create a new folder called "advancedfeedback" in "application/plugins" folder. Inside it create a file called "manifest.php" with the following code:

<?php defined('SYSTEM_PATH') || die('No direct script access allowed.');

// "application/plugins/advancedfeedback/manifest.php"

$params = array(
    'name' => 'Advanced feedback', // Name of our plugin
    'description' => 'Feedback form to let your users contact you.', // Description
    'author' => 'VLD Interactive Inc.', // Author name
    'website' => 'http://www.vldinteractive.com', // Author website
    'version' => '1.0.1', // Plugin version
    'requirements' => array(
        'system' => '1.3.2' // Our plugin will require software version 1.3.2 or higher to run
    ),
);

Create another file called "install.php" that looks like this:

<?php defined('SYSTEM_PATH') || die('No direct script access allowed.');

// "application/plugins/advancedfeedback/install.php"

class Plugins_AdvancedFeedback_Install extends Plugins
{
    public function __construct($manifest = array())
    {
        parent::__construct($manifest);
    }

    public function update()
    {
        return true;
    }

    public function install()
    {
        return true;
    }

    public function uninstall()
    {
        return true;
    }
}

Since our plugin will not really make any changes in the database, installation and other functions in this class don't need to have any code in them. We still have to have this installation class though, even if it doesn't do anything.

Resource files

Now lets create "install" folder inside our plugin folder. This folder will contain all plugin resource files we're going to create now. First place "config.php" file there with the following code:

<?php defined('SYSTEM_PATH') || die('No direct script access allowed.');

// "application/plugins/advancedfeedback/install/config.php"

$config = array(
    'general' => array( // Place settings in the "general" group
        'active' => array( // This setting will enable/disable our feedback form
            'type' => 'boolean',
            'value' => 1
        ),
        'email' => array( // Email to submit the form to
            'type' => 'email',
            'value' => "your@email.com",
            'required' => 1, // Make sure email is required
            'class' => 'input-xlarge'
        ),
        'captcha' => array( // Security captcha image
            'type' => 'select',
            'value' => 1,
            'items' => array(
                '1' => "captcha_enable", // Enable captcha at all times
                '2' => "captcha_disable_users", // Enable only for non-logged in users
                '0' => "captcha_disable" // Disable captcha
            )
        )
    )
);

Create a file called "fields.php" that will store default custom fields. We may later add more custom fields for the feedback form in the control panel once plugin is installed:

<?php defined('SYSTEM_PATH') || die('No direct script access allowed.');

// "application/plugins/advancedfeedback/install/fields.php"

$fields = array(
    0 => array( // field section ID
        'email' => array( // unique field keyword
            'type' => 'text', // text field type
            'required' => 1, // make it required
            'system' => 1, // make sure user cannot change it this field's type and keyword in the control panel
            'multilang' => 0, // we won't have multi-language support here
            'class' => "input-wide", // default css class
            'config' => array(
                'in_view' => 1, // display it by default
                'min_length' => 10,
                'max_length' => 255,
            )
        ),
        'subject' => array( // unique field keyword
            'type' => 'select', // drop down box field type
            'required' => 1, // make it required
            'system' => 1, // make sure user cannot change it this field's type and keyword in the control panel
            'multilang' => 0, // we won't have multi-language support here
            'items' => 4, // this drop down box will contain 5 items which we will add in the related language file.
            'config' => array(
                'in_view' => 1,
            )
        ),
        'message' => array( // unique field keyword
            'type' => 'textarea',
            'required' => 1,
            'system' => 1,
            'multilang' => 0, // we won't have multi-language support here
            'class' => "input-wide input-medium-y", // default css class
            'config' => array(
                'in_view' => 1,
                'min_length' => 10,
                'max_length' => 5000,
            )
        ),
    )
);

Create a file called "meta_tags.php" that will allow us to specify meta tags in the control panel for the feedback page. Since we have only one page to display (contact form itself), this file will contain only 1 item in the array:

<?php defined('SYSTEM_PATH') || die('No direct script access allowed.');

// "application/plugins/advancedfeedback/install/meta_tags.php"

$metatags = array(
    'advancedfeedback_index' => array(
        'picture' => 0 // if set to 1, you will be able to upload a picture on the 'meta tags' tags page in the control panel for this page which will be used for social sharing
    ),
);

Create "navigation.php" file that will allow us to display necessary links:

<?php defined('SYSTEM_PATH') || die('No direct script access allowed.');

// "application/plugins/advancedfeedback/install/navigation.php"

$navigation = array(
    'cp_top' => array( // Control panel navigation bar
        'content' => array( // Parent keyword of this link is "content"
            'content_advancedfeedback' => array( // Keyword for this URL
                'url' => 'system/config/settings/advancedfeedback', // Take user to "settings" page by default
                'attr' => '{"class":"icon-text icon-advancedfeedback"}', // Json encoded attributes
                'order_id' => 0 // Links under "content" tab are ordered by name
            ),
        ),
        'content_advancedfeedback' => array( // Keyword for this URL, matches the one we specified above which will be the parent for this menu
            'content_advancedfeedback_fields' => array(
            'url' => 'system/fields/advancedfeedback', // Take user to "custom fields" page
                'attr' => '',
                'order_id' => 1
            ),
            'content_advancedfeedback_settings' => array(
                'url' => 'system/config/settings/advancedfeedback', // Take user to "settings" page
                'attr' => '',
                'order_id' => 2
            ),
        )
    ),
    'site_bottom' => array( // This indicates that we want to add our link in the bottom navigation bar
        '' => array( // this indicates a parent which we don't have since this is not a drop down menu
            'getintouch' => array( // must be unique
                'url' => 'getintouch', // URL to our feedback page
                'attr' => '{"class":"feedback"}', // Json encoded link attributes
                'order_id' => 0 // Let the system do the ordering by name
            ),
        )
    )
);

// In the control panel our links will looks like this:
// Content (parent tab in the top navigation bar)
// -> Advanced feedback
// -> Custom fields
// -> Settings

Create a file called "permissions.php" that will store our user permissions:

<?php defined('SYSTEM_PATH') || die('No direct script access allowed.');

// "application/plugins/advancedfeedback/install/permissions.php"

$permissions = array(
    'ca' => array( // Front end of the site permissions
        'feedback_access' => array( // unique permission keyword
            'type' => 'boolean',
            'guests' => '1',
            'group_admin' => '1',
            'group_guests' => '1',
            'group_default' => '1',
            'group_cancelled' => '0'
        )
    ),
    'cp' => array( // Control panel permissions
        'fields_manage' => array( // unique permission keyword
            'type' => 'boolean',
            'guests' => '0',
            'group_admin' => '1',
            'group_guests' => '0',
            'group_default' => '0',
            'group_cancelled' => '0'
        ),
        'settings_manage' => array( // unique permission keyword
            'type' => 'boolean',
            'guests' => '0',
            'group_admin' => '1',
            'group_guests' => '0',
            'group_default' => '0',
            'group_cancelled' => '0'
        )
    )
);

And finally, we'll create our language file called "english.php" in the "install/languages" folder to store plugin related language items:

<?php defined('SYSTEM_PATH') || die('No direct script access allowed.');

// "application/plugins/advancedfeedback/install/languages/english.php"

// common language items used throughout the plugin files such as controllers and views
$language = array(
    'advancedfeedback' => array(
        'advancedfeedback' => array(
            'ca' => array( // available on the front end of the site and control panel
                'advanced_feedback' => "Advanced feeback",
                'sender_email' => "Your email",
                'sender_name' => "Your name",
                'message_sent' => "Message has been successfully sent, thank you!",
            )
        )
    )
);

// items for fields.php file
$fields = array(
    0 => array( // field section ID
        'email' => array(
            'name' => "Email",
        ),
        'subject' => array(
            'name' => "Subject",
            'items' => array( // 4 items for our drop down box
                array(
                    'name' => "Advertising",
                ),
                array(
                    'name' => "Sales questions",
                ),
                array(
                    'name' => "Techical support",
                ),
                array(
                    'name' => "Other",
                ),
            ),
        ),
        'message' => array(
            'name' => "Message",
        )
    )
);

// items for meta_tags.php file
$meta_tags = array(
    'advancedfeedback_index' => array(
        'name' => "Contact form",
        'meta_title' => "Get in touch with us",
        'meta_description' => "We love feedback, get in touch with us today!"
    )
);

// items for permissions.php file
// we don't have language items for 'settings_manage' and 'fields_manage' settings,
// it's because we already have those in the software "system" plugin
$permissions = array(
    'feedback_access' => "Access feedback form"
);

// items for config.php file
// note that we don't have language items for 'captcha' setting, it's because we already have it in
// the software "system" plugin so we don't need to include it again to prevent duplicate content and
// extra time for translating the software
$config = array(
    'active' => "Enable feedback form",
    'captcha' => "Security captcha",
    'email' => "Feedback email"
);

// items for navigation.php file
$navigation = array(
    'cp_top' => array( // available in the control panel only, these match keywods in the navigation.php file
        'content_advancedfeedback' => "Advanced feedback",
        'content_advancedfeedback_fields' => "Custom fields",
        'content_advancedfeedback_settings' => "Settings"
    ),
    'site_bottom' => array( // available on the front end of the site and control panel
        'getintouch' => "Get in touch"
    )
);

That's it for our plugin core files setup. Go to the control panel and under "system - plugins" you will see the "Advanced feedback" plugin. Click on "install" link to install it. What this does is populates the database with all of the information we have entered in the files above.

Functionality

If you hover mouse of the "content" tab in the control panel navigation bar, you should see our "Advanced feedback" plugin link.

You may add an icon for the link by creating a "assets/css/cp/advancedfeedback/style.less" file and placing this code in it (of course you'll need to create an icon image as well):

/* use [ and ] instead of curly brackets around base_url */
.icon-advancedfeedback {
    background-image: url('https://www.socialscript.com/assets/images/cp/advancedfeedback/icons/advancedfeedback.svg');
}

Clicking on it will give a "404 not found" error however since we haven't added any actual code to make our plugin work yet. That link will try to display a settings page for the plugin along with its settings (as we have specified in the "navigation.php" resource file). Lets add a controller to do that. Create "advancedfeedback.php" file in the "application/controllers/cp/system/config/settings" folder and insert this code:

<?php defined('SYSTEM_PATH') || die('No direct script access allowed.');

// "application/controllers/cp/system/config/settings/advancedfeedback.php"

// Extend "CP_System_Config_Settings_Controller" controller that contains all necessary functionality
// to display and save plugin settings. All we have to do here is add som basic information to
// the controller
class CP_System_Config_Settings_AdvancedFeedback_Controller extends CP_System_Config_Settings_Controller
{
    public function __construct()
    {
        parent::__construct();

        // Does user have permission to access this plugin?
        if ( !session::permission('settings_manage', 'advancedfeedback') )
        {
            view::noAccess();
        }

        // This will highlight the "Content" tab in the top navigation bar
        view::setCustomParam('section', 'content');

        // This will display navigation links we've added in the
        // "navigation.php" resource file ("settings" and "custom fields")
        view::setCustomParam('options', config::item('cp_top', 'navigation', 'content', 'links', 'content_advancedfeedback', 'links'));

        // Display current location in the "trail" navigation (right under the top navigation bar)
        view::setTrail('cp/system/plugins', __('content', 'system'));
        // The reason we add / at the end of this URI is to prevent duplicate key that's already used in
        // the CP_System_Config_System_Controller to diplay a link to "settings" page in the trail
        view::setTrail('cp/system/config/advancedfeedback', __('advanced_feedback', 'advancedfeedback'));

        // Action hook in case someone else wants to add functionality to this plugin
        hook::action('system/controller/cp_system_config_advancedfeedback');
    }
}

If you try to click on the link again, you should see a page with settings we specified in the "config.php" resource file.

As you can see on the screenshot above, we have 2 navigation links we've added - "custom fields" and "settings". Now that we've created a controller for the settings page, lets create another one for custom fields. Create a file called "advancedfeedback.php" in the "application/controllers/cp/system/fields" folder and insert this code:

<?php defined('SYSTEM_PATH') || die('No direct script access allowed.');

// "application/controllers/cp/system/fields/advancedfeedback.php"

// Extend "CP_System_Fields_System_Controller" controller that contains all necessary functionality
// to display and edit custom fields. All we have to do here is add som basic information to
// the controller
class CP_System_Fields_AdvancedFeedback_Controller extends CP_System_Fields_System_Controller
{
    public function __construct()
    {
        parent::__construct();

        // Does user have permission to access this plugin?
        if ( !session::permission('fields_manage', 'advancedfeedback') )
        {
            view::noAccess();
        }

        // This will highlight the "Content" tab in the top navigation bar
        view::setCustomParam('section', 'content');

        // This will display navigation links we've added in the
        // "navigation.php" resource file ("settings" and "custom fields")
        view::setCustomParam('options', config::item('cp_top', 'navigation', 'content', 'links', 'content_advancedfeedback', 'links'));

        // Display current location in the "trail" navigation (right under the top navigation bar)
        view::setTrail('cp/system/plugins', __('content', 'system'));
        view::setTrail('cp/system/fields/advancedfeedback', __('advanced_feedback', 'advancedfeedback'));

        // Action hook in case someone else wants to add functionality to this plugin
        hook::action('system/controller/cp_system_fields_advancedfeedback');
    }
    
    public function index()
    {
        $this->browse();
    }

    // This controller will be executed when we view custom fields
    public function browse()
    {
        // Set title
        view::setTitle(__('fields', 'system_fields'));

        // Set trail
        view::setTrail('cp/system/fields/advancedfeedback/browse', __('fields', 'system_fields'));

        // Browse custom fields for 'advancedfeedback' plugin
        $this->browseFields('advancedfeedback');
    }

    // This controller will be executed when we edit or add custom field
    public function edit()
    {
        // Get URI vars
        $fieldID = (int)uri::segment(7);

        // Set title
        view::setTitle($fieldID ? __('edit_field', 'system_fields') : __('new_field', 'system_fields'));

        // Set trail
        view::setTrail('cp/system/fields/advancedfeedback/browse/', __('fields', 'system_fields'));

        // Hide certain elements on the page
        $hidden = array('vname' => '', 'html' => 0, 'in_search' => 0, 'in_search_advanced' => 0);
        if ( $fieldID )
        {
            // Get field details from the database
            $field = $this->fields_model->getField($fieldID);
            if ( $field )
            {
                switch ( $field['keyword'] )
                {
                    // hide elements for the 'subject' field and set their default values
                    case 'subject':
                        $hidden['type'] = 'select';
                        $hidden['required'] = 1;
                        $hidden['multilang'] = 0;
                        $hidden['in_search'] = 0;
                        $hidden['system'] = 1;
                        $hidden['config_custom_in_view'] = 1; // field will always be displayed
                        break;

                    // hide elements for the 'email' and 'message' fields and set their default values
                    case 'email':
                    case 'message':
                        $hidden['type'] = 'textarea';
                        $hidden['required'] = 1;
                        $hidden['multilang'] = 0;
                        $hidden['in_search'] = 0;
                        $hidden['system'] = 1;
                        $hidden['config_custom_in_view'] = 1; // field will always be displayed
                        break;
                }
            }
        }

        // Additional field configuration array
        // It will allow us to show/hide custom fields on the feedback form
        $config = array(
            array(
                'label' => __('config_in_view', 'system_fields'),
                'keyword' => 'in_view',
                'type' => 'boolean',
                'rules' => array('intval'),
            ),
        );

        // Run function from the extended controller to display/save custom field
        $this->editField('advancedfeedback', '', 0, $fieldID, $config, $hidden);
    }

    // This controller will be executed when we delete custom field
    public function delete()
    {
        // Get URI vars
        $fieldID = (int)uri::segment(7);

        // Run function from the extended controller to delete custom field
        $this->deleteField('advancedfeedback', '', 0, $fieldID);
    }
}

You should now be able to click on "custom fields" link and add/edit/view fields for the feedback form.

This sums up the functionality we need to have in the control panel. Next step is to create another controller file called "getintouch.php" in the "application/controllers" folder, it will store code to display and submit feedback form:

<?php defined('SYSTEM_PATH') || die('No direct script access allowed.');

// "application/controllers/getintouch.php"

class Contact_Controller extends Controller
{
    public function __construct()
    {
        parent::__construct();

        // Make sure feedback plugin is active
        if ( !config::item('active', 'advancedfeedback') )
        {
            error::show404();
        }
        // Do we have permission to access it?
        elseif ( !session::permission('feedback_access', 'advancedfeedback') )
        {
            view::noAccess();
        }

        // Action hook in case someone else wants to add functionality to this plugin
        hook::action('system/controller/getintouch');
    }

    public function index()
    {
        // Get feedback form fields
        // 'advancedfeedback' is the plugin name
        // '0' is the section ID
        // 'edit' parameter indicates that we're going to display the actual input elements
        // 'in_view' indicates that only fields we've selected will be loaded
        $fields = $this->fields_model->getFields('advancedfeedback', 0, 'edit', 'in_view');

        // Assign $fields variable to the view
        view::assign('fields', $fields);

        // Set meta tags
        $this->metatags_model->setMetaTags('advancedfeedback', 'advancedfeedback_index', array(), true, true);

        // We can optionally set the title of the page which will override the one we set above
        //view::setTitle(__('advanced_feedback', 'advancedfeedback'), false);

        // Load view
        view::load('advancedfeedback/index');
    }
}

What this does is fetches feedback forms from the database, sets page title and meta tags, and displays everything in the "index" view file located in the "application/views/advancedfeedback" folder. So lets create this view file now:

<?
// "application/views/advancedfeedback/index.php"
?>
<? view::load('header'); // loader header tempalte file ?>

<section class="plugin-advancedfeedback advancedfeedback-index">

    <?=form_helper::openForm()?>

        <fieldset>

            <? foreach ( $fields as $field ): // loop through custom fields ?>

                <? if ( $field['type'] == 'section' ): ?>

                    <? view::load('system/elements/field/section', array('name' => text_helper::entities($field['name']))); ?>

                <? else: ?>

                    <? if ( $field['type'] == 'select' ) $field['select'] = true; ?>

                    <div class="control" id="input_row_feedback_<?=( isset($field['system']) ? 'data_' : '' )?><?=$field['keyword']?>">

                        <label for="input_edit_feedback_<?=( isset($field['system']) ? 'data_' : '' )?><?=$field['keyword']?>">
                            <?=text_helper::entities($field['name'])?> <? if ( isset($field['required']) && $field['required'] ): ?><span class="required">*</span><? endif; ?>
                        </label>

                        <div class="field">

                            <? view::load('system/elements/field/edit', array(
                                'prefix' => 'feedback',
                                'field' => $field,
                            )) // this code will load 'system/elements/field/edit' file that displays input fields ?>

                        </div>

                    </div>

                <? endif; ?>

            <? endforeach; ?>

            <? if ( config::item('captcha', 'advancedfeedback') == 1 || config::item('captcha', 'advancedfeedback') == 2 && !users_helper::isLoggedin() ): // show security image ?>

                <div class="control" id="input_row_feedback_edit_captcha">

                    <label for="input_edit_feedback_edit_captcha">
                        <?=__('captcha', 'system')?> <span class="required">*</span>
                    </label>

                    <div class="field">

                        <? view::load('system/elements/field/edit', array(
                            'prefix' => 'feedback',
                            'field' => array(
                                'keyword' => 'captcha',
                                'type' => 'captcha',
                            ),
                            'value' => '',
                        )) ?>

                    </div>

                </div>

            <? endif; ?>

            <div class="control actions">
                <? view::load('system/elements/button'); // load 'submit' button input element ?>
            </div>

        </fieldset>

    <?=form_helper::closeForm(array('do_send_feedback' => 1))?>

</section>

<? view::load('footer'); // loader footer tempalte file

If everything was done right, you should be able to navigate to the feedback form via http://www.example.com/contact in your browser and see the form:

Lets add the code to validate form values now, modify "index" function in the controller and add a new function like this:

public function index()
{
    // Get feedback form fields
    // 'advancedfeedback' is the plugin name
    // '0' is the section ID
    // 'edit' parameter indicates that we're going to display the actual input elements
    // 'in_view' indicates that only fields we've selected will be loaded
    $fields = $this->fields_model->getFields('advancedfeedback', 0, 'edit', 'in_view');

    // Assign $fields variable to the view
    view::assign('fields', $fields);

    // Process form values
    if ( input::post('do_send_feedback') )
    {
        $this->_sendFeedback($fields);
    }

    // Set meta tags
    $this->metatags_model->setMetaTags('advancedfeedback', 'advancedfeedback_index', array(), true, true);

    // We can optionally set the title of the page which will override the one we set above
    //view::setTitle(__('advanced_feedback', 'advancedfeedback'), false);

    // Load view
    view::load('advancedfeedback/index');
}

protected function _sendFeedback($fields)
{
    $rules = array(
        'data_email' => array(
            'label' => __('email', 'users'),
            'rules' => array('valid_email'),
        ),
    );
    // Do we require security image?
    if ( config::item('captcha', 'advancedfeedback') == 1 || config::item('captcha', 'advancedfeedback') == 2 && !users_helper::isLoggedin() )
    {
        $rules['captcha'] = array('rules' => array('is_captcha'));
    }

    // Validate form values
    if ( !$this->fields_model->validateValues($fields, $rules) )
    {
        return false;
    }

    echo '<pre>';
    print_R($_POST);
    echo '</pre>';
    return;
}

If you submit the form now, you will see an output with your submitted values. So finally lets add the code to send whatever user filled in to our email. Modify the "_sendFeedback" function like this:

protected function _sendFeedback($fields)
{
    $rules = array(
        'data_email' => array(
            'label' => __('email', 'users'),
            'rules' => array('valid_email'),
        ),
    );
    // Do we require security image?
    if ( config::item('captcha', 'advancedfeedback') == 1 || config::item('captcha', 'advancedfeedback') == 2 && !users_helper::isLoggedin() )
    {
        $rules['captcha'] = array('rules' => array('is_captcha'));
    }

    // Validate form values
    if ( !$this->fields_model->validateValues($fields, $rules) )
    {
        return false;
    }

    // Compose message variable from our custom fields
    $message = "";
    foreach ( $fields as $field )
    {
        if ( $field['keyword'] == 'subject' )
        {
            $subject = isset($field['items'][input::post('data_subject')]) ? $field['items'][input::post('data_subject')] : 'n/a';
        }
        else if ( $field['keyword'] != 'email' && input::post('data_' . $field['keyword']) !== false )
        {
            if ( is_array(input::post('data_' . $field['keyword'])) )
            {
                if ( isset($field['items']) )
                {
                    $values = array();
                    foreach ( input::post('data_' . $field['keyword']) as $k )
                    {
                        if ( isset($field['items'][$k]) )
                        {
                            $values[] = $field['items'][$k];
                        }
                    }
                    $value = $values ? implode(', ', $values) : 'none';
                }
            }
            elseif ( $field['type'] == 'select' || $field['type'] == 'radio' )
            {
                $value = isset($field['items'][input::post('data_' . $field['keyword'])]) ? $field['items'][input::post('data_' . $field['keyword'])] : 'n/a';
            }
            else
            {
                $value = input::post('data_' . $field['keyword']);
            }
            $message .= $field['name'] . ': ' . $value . "\n";
        }
    }

    // Load email library
    loader::library('email');

    // Set 'reply' email
    $this->email->reply($email);

    // Send email
    $this->email->sendEmail(config::item('email', 'advancedfeedback'), $subject, $message);

    // Success
    view::setInfo(__('message_sent', 'advancedfeedback'));
    router::redirect('getintouch');
}

This function will now loop through submitted fields, compose a $message variable using submitted values, and send off an email. If you add more custom fields to the feedback form in the control panel, they will be automatically displayed and processed.