![]() Join Up! 96815 members and counting! |
|
|||
Making forms object-oriented
Yuri Makasjuk
One of the most common tasks that I came across developing interactive
websites was processing users' input. In this article I would like to share the experience
I had developing an object-oriented
solution for it. The best effect that I can imagine this paper
to have is to not just show another solution for checking a submitted
form, but rather to let you taste the power of object-oriented technology.
Together with you, I will look back at the way I thought that brought me to a
solution that was implemented and successfully used. Reading this article requires
some knowledge of object-oriented programming fundamentals. As a
result, we will
have a working example developed.
Introduction
First of all, we already
know how to examine $HTTP_POST_VARS, we know how to name form inputs in such a
way that they will be presented as a nice PHP array. We've read Richard
Creech's article on making a form's controls dynamic. Why do we need anything else at this stage?
Let me explain. You see,
I have a Delphi background and with Delphi you get very much addicted to re-using
instead of re-inventing. So, if you find yourself coding something that
gives you this dejavu - I've been doing this before - you want to
invent a class that can be reused throughout your projects.
Analysis
Let's take a look at the task of processing user forms and try to find
the most common steps. They will describe generic functionality of a form.
First of all, a form must be presented to user. This includes rendering graphical
representation of form objects (which is carried out by browsers quite efficiently).
Then we must display the initial values; this should be governed by application
logic. Then the user's input must be received and analyzed. We receive a set of values from user.
At the abstract level the decision that is made on this stage would usually be:
can this form be considered completed or should we go back
and ask user to correct something.
Let's first look at the situation when we
need something corrected. In this case an application must display the form
again and give some explanations about what went wrong (display some error messages).
Besides, a well-mannered form should preserve values entered by user, at least
the correct ones. Now suppose, we decide that the
values are good enough to allow us to continue.
Then these values (or some of them) usually get stored somehow (in a database, in
session variables). Finally, another decision is made: where do we go from here? It
is often based on values entered and, of course, follows some paths defined
by programmer.
Now, what should we try to achieve by the object analysis? The best would be
to program generic behavior in parent class(es) once and forever. Then from that
moment on we will be able to only concentrate on context-specific steps for each new
form that we develop: check values, store values, decide where to go from the
form.
The key element will be a generic form class that I called FormProcessor. Let's
first have a general idea about how it works. I like putting forms into separate
files. There's nothing that makes it necessary, it just makes things easier,
for example, to edit them in visual editors. Also, I usually have forms that
'submit to themselves', in other words I usually group a form with code that
takes its input. One idea is to create instances of FormProcessor's sub-classes
in the beginning of such files. Then, this instance will 'tune in' the environment
and will be able to have complete control over displaying the
form and working with it further on. By 'tuning in', I mean deciding whether
this particular form was displayed and submitted before and we should now check
the input. If it has never been submitted before, it should be displayed for the
first time. Object-oriented technology will work for us in both situations.
Generic framework within FormProcessor will run one of the virtual member functions
-- either for checking or for displaying the form. Notice: all we will have
to do in each particular form's class is override these virtual functions.
In the same way, we will override the function responsible for storing form's
values. Again, generic class will decide when to call it.
Executing environment
Before we go deeper into details, there's one important thing to talk about.
That is, how do we organize forms into a branching sequence (I hope, nobody
minds me using word 'wizard' here)? Here's what I suggest. You may think of
it as of some kind of a 'runner' that manipulates instances of classes derived from FormProcessor.
This environment knows which form (script in a separate file) should
be loaded first. It then includes the script containing a form (FormProcessor's
descendant). This form decides whether it is still displaying itself or
just finishing processing of data submitted to it. After storing correct data it reports to the runner:
hey, I'm finished now -- look, this is who should be next. Based
on this information, the runner can load the next form, according to
the decision made in the current one. The next form, when included, will sense that it is just loaded for
the first time. It will notify the environment (runner) about that and the chain will
stall at this point. Of course, waiting for the next form to be submitted. Before
something else distracts us, let's see what I meant here in the code:
Now, if you put the code above in a separate file, you may then include that
file in your nicely formatted page. This page will be displaying your wizard's
screens.
Implementing the FormProcessor class
But this was still quite far from our original subject, we were talking about
object-oriented forms here, remember? Let's have a look at FormProcessor and
see what's under its hood. I had to strip the code of PHPDoc-style comments,
because I think it requires step-by-step explanations, while reading through
the comments in arbitrary order might confuse. Version of this source with PHPDoc
comments is available for download from
here.
One of the key principles in FormProcessor's functionality is that it knows
how HTML form's inputs are named. Later on in this article, I will show how we
automate naming of fields, now just take it as it is: FormProcessor's descendants
know name of the array that form's values are submitted into. Inside the member
functions of the class, this name is evaluated as...
$this->fieldPrefix . $this->Name
...where
$this->Name is form's name that we pass as constructor's parameter.
$this->fieldPrefix is defined as data member of the class, but you can
think of it as of a constant value that remains the same throughout lifetime
of instances of the class. Its only purpose is to transparently add 'uniqueness'
to the field names, so that they do not accidentally get mixed with some other
program's objects.The first thing generic constructor does is it 'smells the air around'. It assigns
global variable (whether it is set or not) with this magic name to $this->Values.
Then, if $this->Values is not empty, i.e. form's fields were submitted before,
FormProcessor decides that it is the not the first time this form is on the
arena. In this case, it calls the abstract $this->CustomCheck() member function.
Overriding this function in sub-classes allows these classes' authors to implement
whatever custom conditions checking they fancy. Of course, being a member function
of the class gives CustomCheck() access to $this->Values array, which contains
all values that form is working with as an associative array.
If overridden CustomCheck() finds error(s) that should not allow the form to
be considered as completed, it should write error messages in $this->Errors
data member. It is also treated as an associative array with the same association
as $this->Values. In other words, each submitted value has a potential error message
linked with it which, if not empty, indicates a fault in this value.Then, and this is my favorite thing about abstract generic classes, the rest
of constructor reads like English: if the form is completed, let's somehow store
the data it has. Otherwise, let's display it (again or for the first time -
IsCompleted() returns 'false' if this is the form's first time). Of course,
StoreData() and DisplayForm() are calls to abstract member functions that should
be overridden in sub-classes. I hope you have noticed that in case form is
completed, besides storing data constructor also assigns a new value to the
global variable $wizardPage. This action tells the 'runner' - executing environment
- what form it should load next ('while' loop in runner will make another iteration).
Can you guess where the next $wizardPage's value comes from? Of course, from
an abstract function that should get overridden in a sub-class.
Meaning of the rest of the FormProcessor's machinery should become clear from
the following discussion. And now, before we attempt to build something useful
with our class, I have to explain how HTML form's inputs know the names they
should be assigned.
Inputs - brief introduction
In fact, auto-assigning inputs' names really was the initial idea that pushed
me towards developing of another set of classes. The classes that would serve as
object wrappers for HTML form's input controls. In this article I would really
like us to concentrate on the FormProcessor class and its descendants. Developing of a
properly analyzed hierarchy of classes for form controls deserves
a separate discussion. So, I suggest that we only talk about form controls classes
as much as we need for completing this study.
As you can see, constructor of the base class (FormControl) does all the job
of linking the control to form object, reference to which is passed as one of
parameters. Then, the name of HTML input is evaluated. It should be
such that the value will be submitted as part of an array which the form (descendant
of FormProcessor) will be looking for. Name of input is then stored into a local
data member. Control also receives the default value as one of constructor's
parameters. This value will be used if the form that
contains the control is displayed for the first time. Control is accessing a
data member of form class to check this. Otherwise, the default value will be taken
from the form's $Values array.
Then the constructor calls the Display() member function, which echoes output of
the Render() function. This function is abstract and should be overridden in
sub-classes so that they produce correct HTML code to represent themselves in
pages. See definitions of the two other classes: TextInput and RadioButtons
for illustration of this.
Example of a form
Now we are equipped for the final touch: building a small example. I will demonstrate
creation of the first page of a wizard that should collect some information. Let's
say, about a subscription to some internet resource. As the first step,
the user must enter the username he is going to use, correct email address and select
type of the registration. Based on the registration type user chooses,
the wizard
should proceed to a corresponding screen. Before the wizard can proceed, it validates
values that the user enters. I will not build the forms for the second step. There's
not much interesting to do without introducing additional input
classes; and I would still like the article's subject to stay in the center of
discussion.
<form action="<?php echo $PHP_SELF?>" method="POST">
Username: <?php new TextInput($this, 'Name', '', 'size="30" maxlength="100"')?>
<?php $this->ErrorReport('Name')?>
This code can also be downloaded from here.
Conclusion
Object-oriented programming is an efficient and powerful technique. It allows
one to create reusable, easily adjustable components that make programming process
fast and, honestly, very much fun, too. I hope, my little experience that I
presented here will inspire you to build your own libraries of classes that
would be a basement for complex and sophisticated web applications.
-- Yuri
|