PDA

View Full Version : How to Avoid Complicated PHP Forms and Data Processing



Aqua
05-13-2007, 02:50 PM
I have collected some Info on this. its not My written work ;)

When you are creating a web application forms and data should be held strictly seperate. In the PHP community this rule is ignored and PHP itself is being abused. Template and framework system developers have made it more commonplace. CMS developers seem to have forgotten that under no circumstances should the code used to display forms be allowed to process the data involved. Here's why not and the solution.

Complicated code

Coding a mixture of form display, validation and data handling makes for some very complicated code. No matter what you do it will just look like a rows of blah..blah..blah and take hours to pick up on the logic. This is the whole point to writing good code. To be able to pick up the code page at anytime and know what it does is just as satisfying as seeing it run without bugs.

Templating

Usability of forms is important. It may be that to get really good usability and flow in a form or multiple forms you may have to design them and rework them dozens of times. This kind of work is gruelling if you have to also program data processing and validation at the same time. It just takes longer and more mistakes can be made.

Mixing display and processing removes any possibility of using HTML templating.
Troubleshooting and scaling

Can you say spagetti code? Trying to troubleshoot spagetti code is what PHP coders are frustrated by the most. Beginners and longtime pros all agree on this.

When forms display is held to template code and the business logic, validation and data processing held to seperate code then scaling is easy. You can change your SQL statement and know that the display of the data will be the same. You can tweak the validation or pick new elements for validation without touching the form display. Lastly you can design a form or have someone else do it one day and then the next day work on the data processing and validation. Your day to day work flow becomes less complicated and less stressful.
A comparison of techniques

Let take a look at the three different ways that some content management systems have handled the task.
Drupal

There are many that think Drupals code is slick and easy. But if you take a look at the function below you may find it very daunting to take on the task of making changes to the form. Drupal tries to take this into account by providing a system, an API as many other templating projects do. But the system itself leads to more complication and requires that you understand fully what is happening in the original forms PHP code. All this takes time and more time. It is also not something that you can leave up to a web designer that is not familiar with PHP.


<?php

function node_form_array($node) {
node_object_prepare($node);
// Set the id of the top-level form tag
$form['#id'] = 'node-form';
/**
* Basic node information.
* These elements are just values so they are not even sent to the client.
*/
foreach (array('nid', 'vid', 'uid', 'created', 'changed', 'type') as $key) {
$form[$key] = array('#type' => 'value', '#value' => $node->$key);
}
// Get the node-specific bits.
$form = array_merge_recursive($form, node_invoke($node, 'form'));
if (!isset($form['title']['#weight'])) {
$form['title']['#weight'] = -5;
}
$node_options = variable_get('node_options_'. $node->type, array('status', 'promote'));
// If this is a new node, fill in the default values.
if (!isset($node->nid)) {
foreach (array('status', 'moderate', 'promote', 'sticky', 'revision') as $key) {
$node->$key = in_array($key, $node_options);
}
global $user;
$node->uid = $user->uid;
}
else {
// Nodes being edited should always be preset with the default revision setting.
$node->revision = in_array('revision', $node_options);
}
$form['#node'] = $node;
if (user_access('administer nodes')) {
// Node author information
$form['author'] = array('#type' => 'fieldset', '#title' => t('Authoring information'), '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => 20);
$form['author']['name'] = array('#type' => 'textfield', '#title' => t('Authored by'), '#maxlength' => 60, '#autocomplete_path' => 'user/autocomplete', '#default_value' => $node->name ? $node->name : '', '#weight' => -1, '#description' => t('Leave blank for %anonymous.', array('%anonymous' => theme('placeholder', variable_get('anonymous', 'Anonymous')))));
$form['author']['date'] = array('#type' => 'textfield', '#title' => t('Authored on'), '#maxlength' => 25, '#description' => t('Format: %time. Leave blank to use the time of form submission.', array('%time' => $node->date)));
if (isset($node->nid)) {
$form['author']['date']['#default_value'] = $node->date;
}
// Node options for administrators
$form['options'] = array('#type' => 'fieldset', '#title' => t('Publishing options'), '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => 25);
$form['options']['status'] = array('#type' => 'checkbox', '#title' => t('Published'), '#default_value' => $node->status);
$form['options']['moderate'] = array('#type' => 'checkbox', '#title' => t('In moderation queue'), '#default_value' => $node->moderate);
$form['options']['promote'] = array('#type' => 'checkbox', '#title' => t('Promoted to front page'), '#default_value' => $node->promote);
$form['options']['sticky'] = array('#type' => 'checkbox', '#title' => t('Sticky at top of lists'), '#default_value' => $node->sticky);
$form['options']['revision'] = array('#type' => 'checkbox', '#title' => t('Create new revision'), '#default_value' => $node->revision);
}
else {
// Put all of these through as values if the user doesn't have access to them.
foreach (array('uid', 'created') as $key) {
$form[$key] = array('#type' => 'value', '#value' => $node->$key);
}
}
// Add the buttons.
$form['preview'] = array('#type' => 'button', '#value' => t('Preview'), '#weight' => 40);
$form['submit'] = array('#type' => 'submit', '#value' => t('Submit'), '#weight' => 45);
if ($node->nid && node_access('delete', $node)) {
$form['delete'] = array('#type' => 'button', '#value' => t('Delete'), '#weight' => 50);
}
$form['#after_build'] = array('node_form_add_preview');
return $form;
}
?>
Joomla

This code while easier to handle still requires that the designer know the system intimately. While there is very little PHP code involved it is code that is propietory to the CMS. This may not take much time to change but it does require that lots of time be spent in the documentation beforehand. Of the three examples shown Joomla comes closest to providing a true HTML based template.


<script type = "text/javascript">
<!--
var link = document.createElement('link');
link.setAttribute('href', 'components/com_poll/assets/poll_bars.css');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('type', 'text/css');
var head = document.getElementsByTagName('head').item(0);
head.appendChild(link);
//-->
</script>
<form action="index.php" method="post" name="poll" id="poll">
<?php if ($this->params->get( 'page_title')) : ?>
<div class="componentheading<?php echo $this->params->get( 'pageclass_sfx' ) ?>">
<?php echo $this->params->get( 'header') ?>
</div>
<?php endif; ?>
<div class="contentpane<?php echo $this->params->get( 'pageclass_sfx' ) ?>">
<label for="id">
<?php echo JText::_('Select Poll'); ?>
<?php echo $this->lists['polls']; ?>
</label>
</div>
<div class="contentpane<?php echo $this->params->get( 'pageclass_sfx' ) ?>">
<?php echo $this->loadTemplate('graph'); ?>
</div>
</form>
Siteframe

Using Smarty and other templating systems is not a good solution. I have heard many things about Smarty both good and bad. Many swear by it because of the way it handles the seperation of logic. But looking at it here in this snippet I find myself in shock. This is not something I want to open up when I am short on time and have to come up with a quick fix.


<?php

{* $Id: form.tpl,v 1.4 2005/12/06 18:58:18 glen Exp $
** Copyright (c)2004, Glen Campbell. All rights reserved.
** This template displays an input form
*}
<form id="{$form_name}" action="{$form_action}" method="{$form_method|default:"post"}" class="defaultform" {if $form_enctype}enctype="{$form_enctype|default:"multipart/form-data"}"{/if}>
{section name=item loop=$form_elements}
{if $form_elements[item].advanced && !$_adv}
{assign var=_adv value=true}
<p class="adv">
Click to display additional settings (#adv)</p>
<div id="form_advanced" style="display:none;">
{/if}
{if $form_elements[item].hidden}
<input type="hidden" name="{$form_elements[item].name}" value="{$form_elements[item].value}" />
{else}{* these require prompts and wrappers *}
<label>{$form_elements[item].prompt}{if $form_elements[item].required}<span style="color:red;">*</span>{/if}</label>
<div class="item">
{if ($form_elements[item].type eq "text")||($form_elements[item].type eq "number")}
<input type="text" name="{$form_elements[item].name}"
{if $form_elements[item].type eq "number" || $form_elements[item].value ne ""}value="{$form_elements[item].value|escape}"{/if}
{if $form_elements[item].size ne ""}size="{$form_elements[item].size}"{/if}
{if $form_elements[item].maxlength ne ""}maxlength="{$form_elements[item].maxlength}"{/if}
{if $form_elements[item].disabled ne ""}disabled="disabled"{/if}
/>
{elseif $form_elements[item].type eq "password"}
<input type="password" name="{$form_elements[item].name}"
{if $form_elements[item].size ne ""}size="{$form_elements[item].size}"{/if}
{if $form_elements[item].maxlength ne ""}maxlength="{$form_elements[item].maxlength}"{/if}
{if $form_elements[item].disabled ne ""}disabled="disabled"{/if}
/>
{elseif $form_elements[item].type eq "textarea"}
<textarea name="{$form_elements[item].name}"
{if $form_elements[item].rows ne 0}rows="{$form_elements[item].rows}"{/if}
{if $form_elements[item].cols ne 0}cols="{$form_elements[item].cols}"{/if}
{if $form_elements[item].disabled ne ""}disabled="disabled"{/if}
{if $form_elements[item].formatted}class="widgEditor"{/if}
>{$form_elements[item].value|escape:"html"}</textarea>
{elseif $form_elements[item].type eq "checkbox"}
<input type="checkbox" name="{$form_elements[item].name}" value="{$form_elements[item].rval}"
{if $form_elements[item].value}checked="checked"{/if}/>
{elseif ($form_elements[item].type eq "select")||($form_elements[item].type eq "category")}
<select
{if $form_elements[item].multiple}name="{$form_elements[item].name}[]" multiple
{else}name="{$form_elements[item].name}"
{/if}
{if $form_elements[item].onchange}onChange="{$form_elements[item].onchange}"{/if}
{if $form_elements[item].size}size="{$form_elements[item].size}"{/if}>
{*if !$form_elements[item].multiple}
<option label="[none]" value=""> -none- </option>
{/if*}
{html_options options=$form_elements[item].options
selected=$form_elements[item].value}
</select>
{elseif $form_elements[item].type eq "date"}
{html_select_date
prefix=$form_elements[item].name
start_year=-50
end_year=+20
month_empty="Month"
day_empty="Day"
year_empty="Year"
time=$form_elements[item].value
}
{elseif $form_elements[item].type eq "datetime"}
{if $form_elements[item].checkbox ne ""}
<input type="checkbox" name="{$form_elements[item].checkbox}" value="1"
{if $form_elements[item].checkbox_value}checked="checked"{/if}/>
{/if}
{html_select_date
prefix=$form_elements[item].name
start_year=-50
end_year=+20
month_empty="Month"
day_empty="Day"
year_empty="Year"
time=$form_elements[item].value
}
{html_select_time
prefix=$form_elements[item].name
time=$form_elements[item].value
use_24_hours=true
}
{elseif $form_elements[item].type eq "file"}
<input type="file" name="{$form_elements[item].name}{if $form_elements[item].multiple}[]{/if}"/>
{if $form_elements[item].caption}
<label>Caption:</label>
<input type="text" name="{$form_elements[item].name}_caption{if $form_elements[item].multiple}[]{/if}" size="20"/>
{/if}
{else}
Unsupported data type {$form_elements[item].type}
{/if}
{if $form_elements[item].help ne ""}
<p class="help">{$form_elements[item].help}</p>
{/if}
</div>{* class="item" *}
{/if}
{/section}
{if $_adv}
</div>
{/if}
{* handle file attachments/detachments *}
{if is_array($obj) && is_array($obj.associated_files)}
<label>{lang name="prompt_file_attachments"}</label>
{section name=i loop=$obj.associated_files}
<div class="item"><input type="checkbox" name="del_file[]" value="{$obj.associated_files[i].file_id}" />
{$obj.associated_files[i].file_path|basename}</div>
{/section}
<p class="help">{lang name="help_file_attachments"}</p>
{/if}
<input type="submit" value="{$form_submit}" onClick="form.submit(); swap('{$form_name}','wait');"/>
<input type="reset" value="{$form_reset}" />
</form>
<div id="wait" style="display:none;">
{if $script ne "setup.php"}
{random class="Quote"}
<p class="quotation">{$quote.quote_text}


(submitted by {$quote.user.user_name})
</p>
{/random}
{/if}


One moment, please....</p>
</div>
?>
The solution

Doing forms and processing their data in an uncomplicated manner is something that I saw frequently in the world of Classic ASP but it is a rarity in the PHP community. It is simple. Send the form to a validation script then a processing script. The basic code is a snap. You only have to allow for a changing form and stick with some naming conventions so that the proper information is sent via the $_POST object. This is the key to validating and processing.


<?php

foreach($_POST as $key => $data) {
if($key != 'required') {
if($data !='') {
$message .= $key.": ".$data ."\n";
}
}
}
?>
form.php

This is a plain HTML page with a form and the action goes to form_valid.php. There is no PHP or anythingelse to complicate the designing or manipulation of the form. If the web developer dare they could elect to use small blocks of PHP to hold in formation in session. this is the reason for using a PHP file extension. To allow for a tiny bit of code if necessary. This is so that if the user messes up they are not presented with a blank form. Only the elements are emptied. Even doing this would make templating a form so easy any HTML hack could do it.
form_valid.php

This is where things are made simple as copying and pasting. When the form.html elements are recieved the code just runs through them and checks them for valid information and formating if an error or erroneous information is found then it redirects back to the form.php. The best thing about this is that the script can be written generically so that any form can be validated based on the data sent. A single area for all validation means cleaner code.
form_process.php

This is where all the good stuff happens. SQL statements are processed and then the user is redirected to the view or next process.
Conclusion

In an effort to be bleeding edge and to reuse code CMS web developers have forgotten about the basics. Why they choose to go in for highly complicated solutions that do not reduce the number of code lines or simplify the applications design is beyond me. Why insist that everything be created dynamically, on-the-fly or stored in a database? But it is something that can be fixed easily and quickly. Just drop all the fancy manipulations, OOP and design patterns and go back to writing PHP.

a_bertrand
05-15-2007, 07:23 PM
Few thoughts:

- Small code = more readable
- Small code = faster code (normally)
- More readable = less bugs
- Less bugs = more secure

Now... On the other side, think about complex forms, where you have 20 fields, having a form -> check / error -> back is not really nice, where having a form saying on the side of the row where the error is, is much nicer, and easier to use for your users.

So, some times a small easy fast code need to be traded against good user interfaces. And that's usually the problem, when you want to have a good user interface, you start coding a lot of JavaScript for example, which certainly does make the code easier to read, but can some times provide a completely new user experience. And... up to a certain level, we are all pointing to a better user experience, isn't it?

So my 2 cents are: keep it simple if possible, but always balance simplicity and good user interface. Some times the second one is much more important than the first one.