I figured out why I'll never be happy w/ php forms libraries

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

jmut
Forum Regular
Posts: 945
Joined: Tue Jul 05, 2005 3:54 am
Location: Sofia, Bulgaria
Contact:

Re: I figured out why I'll never be happy w/ php forms libraries

Post by jmut »

Well arborint, ZForm can do all this with little to no hassle.
Can configure the form using XML, ini files, array .. whatever... can easily make it to produce XML, XHTML or whatever... (if not built in, then it's totally easy to extend)

1. The entire form and surrounding XHTML is generated with PHP (like with QuickForm or Zend_Form)
#Not necessarily. Up to you to decide which approach. ZForm can do
2. The form layout is XHTML, but the form fields are generated with PHP. (inghamn's example does this)
#ZForm can do
3. The form layout and form fields are all in XHTML and the PHP only sets values or selections (Kieran style)
#ZForm can do..you cannot go without any php in html anyhow

Ok I may sound as if I created this ZForm..and I didn't , but I really think this is trully a piece of art. I wonder if people discussing here have tried it out....would really like to hear the drawbacks..... building form in php is not one of them! Only thing I see is perhaps some performance issue wich with this pre generating of template is boosted...but I doubt it's better than the maintainability you get.
Now I must say, that I haven't tried out theirs too...but I don't see lacks in ZForm - it easily makes me do the whole process(validation etc etc)....it is never only about rendering html.
Last thing I am trying todo here is make you think ZForm is panacea but I personally thing it's really good thought of component, and it's just in its infancy.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: I figured out why I'll never be happy w/ php forms libraries

Post by Christopher »

I think I have agreed with you on this point several times above. I think it would be good to show examples of Zend_Form solving the above three styles of forms. Thought I wonder about your comment "you cannot go without any php in html anyhow."

You have experience with Zend_Form. Would you implement some demo code that we could all look at and run? Maybe based on some of the examples in the manual?
(#10850)
jmut
Forum Regular
Posts: 945
Joined: Tue Jul 05, 2005 3:54 am
Location: Sofia, Bulgaria
Contact:

Re: I figured out why I'll never be happy w/ php forms libraries

Post by jmut »

Added in my todo.
Just might not be before next Wednesday(23.04).... going hometown for holidays...and tons of stuff to finish and prep today.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Re: I figured out why I'll never be happy w/ php forms libraries

Post by Chris Corbyn »

I'm interested in following this thread. The best form handling library I've ever seen (and use daily) isn't open to the public yet. But it's very neat and very mature (3 years+ I think). I didn't write it; it's written by one of the team leaders at SitePoint and will (probably) be released to the public before much longer since he's got permission to open source it.
webaddict
Forum Commoner
Posts: 60
Joined: Wed Mar 14, 2007 6:55 am
Location: The Netherlands

Re: I figured out why I'll never be happy w/ php forms libraries

Post by webaddict »

Chris Corbyn wrote:I'm interested in following this thread. The best form handling library I've ever seen (and use daily) isn't open to the public yet. But it's very neat and very mature (3 years+ I think). I didn't write it; it's written by one of the team leaders at SitePoint and will (probably) be released to the public before much longer since he's got permission to open source it.
Keep us posted!
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

Re: I figured out why I'll never be happy w/ php forms libraries

Post by Luke »

Yes, do keep us posted Chris. I'm very interested to see it.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: I figured out why I'll never be happy w/ php forms libraries

Post by Christopher »

jmut wrote:Added in my todo.
Just might not be before next Wednesday(23.04).... going hometown for holidays...and tons of stuff to finish and prep today.
I'll kick it off with code from the manual modified so people can run it standalone.

Code: Select all

<?php
include_once 'Zend/View.php';
include_once 'Zend/Form.php';
 
$form = new Zend_Form;
$form->setAction('/usr/login')
          ->setMethod('post');
 
// Create and configure username element:
$username = $form->createElement('text', 'username', array('label' => 'Username'));
$username->addValidator('alnum')
         ->addValidator('regex', false, array('/^[a-z]+/'))
         ->addValidator('stringLength', false, array(6, 20))
         ->setRequired(true)
         ->addFilter('StringToLower');
 
// Create and configure password element:
$password = $form->createElement('password', 'password', array('label' => 'Passsword'));
$password->addValidator('StringLength', false, array(6))
         ->setRequired(true);
 
// Add elements to form:
$form->addElement($username)
     ->addElement($password)
     // use addElement() as a factory to create 'Login' button:
     ->addElement('submit', 'login', array('label' => 'Login'));
 
 
echo $form->render(new Zend_View());
Which produces (I cleaned up the output to make it more readable):

Code: Select all

<form enctype="application/x-www-form-urlencoded" action="/usr/login" method="post">
<dl class="zend_form">
<dt><label for="username" class="required">Username</label></dt>
<dd><input type="text" name="username" id="username" value=""></dd>
<dt><label for="password" class="required">Passsword</label></dt>
<dd><input type="password" name="password" id="password" value=""></dd>
<dt></dt>
<dd><input type="submit" name="login" id="login" value="Login"></dd>
</dl>
</form>
So pretty good so far. A couple of things I notice immediately.

- I needed to pass it a Zend_View to make it work. It seems like it is wired into their View system.
- The dl/dt/dd tags may seem a little strange. Most people would use div or label I think.
- We might want to set the class property for form fields and tags containing them. And the dl has a class, but no id.
(#10850)
User avatar
John Cartwright
Site Admin
Posts: 11470
Joined: Tue Dec 23, 2003 2:10 am
Location: Toronto
Contact:

Re: I figured out why I'll never be happy w/ php forms libraries

Post by John Cartwright »

arborint wrote:So pretty good so far. A couple of things I notice immediately.

- I needed to pass it a Zend_View to make it work. It seems like it is wired into their View system.
- The dl/dt/dd tags may seem a little strange. Most people would use div or label I think.
- We might want to set the class property for form fields and tags containing them. And the dl has a class, but no id.
This is enough to make me not even want to look at it. Ugh.
User avatar
Mordred
DevNet Resident
Posts: 1579
Joined: Sun Sep 03, 2006 5:19 am
Location: Sofia, Bulgaria

Re: I figured out why I'll never be happy w/ php forms libraries

Post by Mordred »

Wow, what a thread!
I read it all, but I didn't get all points along nor I remembered most of it, so excuse me if I say something already said, or too irrelevant.

My form generation is based on the scaffolding code, so I'll have to explain bits of my entire framework here.
As I already mentioned in the scaffolding thread, I want to generate everything. Since that post the system has evolved a bit more, and since having written several small apps it seems okay. I haven't shipped a commercial project yet, so it's not fire tested ;)

Here's a few words for my "framework":
I don't do MVC, something similar (or maybe it's the same and I don't get what MVC is) but not quite. I have models and controllers and the view is either a fiew lines of in-controller code or view/controller class + templates. Forms and all user input actually is handled by "widgets" - a mix of subcontrollers and subviews in MVC terminology (or I think so - does it make sense to you MVC guys?) which are easily "married" to templates and models. The widgets can act on user input and draw themselves. Simplest widget - gets a certain $_REQUEST value and renders as text input with the value set as default.
My design philosophy is to make the library in a way that allows maximally laconical code. Also there are some old pieces of code in it that do things a bit differently, and I happily mix both styles. For example my scaffolding code generates forms through templates, but my "db table" widget (older code) also generates forms, but directly in PHP. There are lots of things I haven't implemented at all - like client-side checks, but I know that the system is easily modifiable to do so. Some things are not translated, as the my translation system is still in tryouts :)

I have setup a quick online demo. May contain bugs, as the scaffolding is still in infancy. There are some XSS possibilities in the dbtable. No SQL injection should be possible. The array at the bottom of the page is the SQL query log. You can reset the tables from the SQL submenus, it's intentional (VERY useful for when you change the field descriptions). And yeah, that's PHP4, hence the &-s.

So, here's some code:

Code: Select all

$g_pSql->m_pFields =& new CTableSql('Fields', 'Fields'); //sql table, class name
$g_pSql->m_pLinked =& new CTableSql('Linked', 'Linked');
 
$g_pSql->m_pFields->AddColumn('m_nId', 'primary', true);
$g_pSql->m_pFields->AddColumn('m_nInt', 'int', false);
$g_pSql->m_pFields->AddColumn('m_sString5', 'string', false, 5);
$g_pSql->m_pFields->AddColumn('m_sDate', 'date', false);
 
$g_pSql->m_pLinked->AddColumn('m_nId', 'primary', true);
$g_pSql->m_pLinked->AddColumn('m_nOtherId', 'int', false);
$g_pSql->m_pLinked->AddOneToOne('m_sOtherString', $g_pSql->m_pFields, 'm_sString5', Array('m_nOtherId'=>'m_nId'));
 
Wanna see some more I've written for this demo? There is none. Everything you see in the demo is generated from these 9 lines of code.

As you see (apart from the fact that I use Hungarian notation ;) ) these define two tables with some fields, and the second table is in a one to one relationship with the first. Actually I haven't renamed my functions since I've written the scaffolding code (it started as an SQL generator), but these lines do more than SQL. In fact they do:
Code generation for models, controllers, and controller/views.
Templates for displaying, editting, updating and printing.
SQL generation, both for runtime queries and .sql files if you want to setup the databases manually
The menu items are added automatically, and as you can see one of them also shows the generated SQL and can be used when developing to update the columns.
The "List" submenus show my other way of doing things - the "db table" widget. It does CRUD, searching, sorting, pagination (visible only for many items) and can do custom actions - by default "custom edit" and "print" which are currently labeled in Bulgarian. The idea is to use this widgets for admin panels and the template-based stuff for the fancy shmancy forms the users would see. The templates I generate are ugly, but functional - they contain placeholders for all the fields and error messages. One can make the template beautiful, or take an already beautiful html and paste the variables from the generated template files.

Here's a snippet from the constructor of the "custom edit" view/controller object (actually a widget too), demonstrating a widget in action:

Code: Select all

$this->m_pFields =& new Fields( GetInt( $_REQUEST['id'] , NULL) ); //create the model object
$this->m_bEditOrAdd = ($this->m_pFields->m_nId != NULL); //are we editting it, or adding a new one?
...
//here's how the int field is handled:
$this->m_nInt =& new CInputTextWidget( $this->GetVarName('m_nInt') , $this->m_pFields->m_nInt); 
$this->m_pFields->m_nInt = $this->m_nInt->Value();
First the widget is created, first param is name (used as index in $_REQUEST), second is default value. The default value is the one that the m_pFields model had when it was filled from the database. The second line gets the actual value from the widget and updates the model. Below that I check if the user is submitting a form - if so I call $this->m_pFields->Update() (or ->Create() ), otherwise nothing. This was the controller part of the object. The view part comes in the form of a ToString() method, which just inits a template with the widget objects.

Here's the print template (the simplest one):

Code: Select all

<hr>m_nId = [[m_nId]]<br>
m_nInt = [[m_nInt]]<br>
m_sString5 = [[m_sString5]]<br>
m_sDate = [[m_sDate]]<br>
<hr>
Here's the relevant model functions:

Code: Select all

   function Update() {
        if ( !$this->CanUpdate() )
            return FALSE;
        $pSql =& Fields::Sql();
        DBQuery( $pSql->GetUpdate('all', $this->m_nId, $this) );
    }
    function CanUpdate() {
        $pSql =& Fields::Sql();
        foreach ($pSql->m_aColumns as $sName=>$pType) {
            if ( !$pType->IsValidInput($this->$sName, false) )
                return FALSE;
        }
        return TRUE;
    }
And here's the descriptor class responsible for handling the "int" type, so you can see the IsValidInput check:

Code: Select all

class IntDescriptor extends BaseDescriptor {
    function IntDescriptor($sType, $default) {
        BaseDescriptor::BaseDescriptor($sType, $default); //true = always blank when inserting!
    }
    function IsValidInput($sValue, $bInsert) {
        if ($sValue=='')
            return ($this->m_Default !== false); //if the default value is false, do not allow empty values
        $sValue = $this->AdjustInput($sValue);  
        return ctype_digit("$sValue");
    }   
    function AdjustInput($value, $bForSearch = FALSE) {
        return intval($value);
    }   
    function ToDefinitionSql($sColumn) {
        return "`$sColumn` INT(11) NOT NULL";
    }               
}
To summarize, I seem to belong to the crowd that does custom, template-based layouts, but generates the form fields. The "rules and filters" arborint talks about are still in a crude form, but they stem from the description code I pasted. I haven't thought much for a more custom rules - but everything should be possible by writing a descriptor class. Here's a more complex example that handles ip-s:

Code: Select all

class IpDescriptor extends StringDescriptor {
...
    function IsValidInput($sValue, $bInsert) {
        $a = explode('.', $sValue);
        if (count($a) != 4) return FALSE;
        for ($i=0; $i<4; ++$i) {
            if (!ctype_digit($a[$i]) || strlen($a[$i])>3) return FALSE;
            if ((int)$a[$i] > 255) return FALSE;
        }
        return TRUE;
    }   
...
}; 
Phiew, that's some LONG post for so short a piece of code ;) I'd like to compare how you MVC guys would handle these and how many LOC it would take with your framework of choice.
User avatar
VladSun
DevNet Master
Posts: 4313
Joined: Wed Jun 27, 2007 9:44 am
Location: Sofia, Bulgaria

Re: I figured out why I'll never be happy w/ php forms libraries

Post by VladSun »

//a little off topic maybe ;)
@Mordred
Nice work :)

Just to share my experience with my "db table widget" (and yeah, I must admit that I'm happy with it):
- it's especially designed for time period based reports and to be used with CodeIgniter framework;
- it has time period choose form and field filter form with validation (also, with all of these fancy jQuery controls);
- it's sortable by every column;
- it has "summary/totals" block;
- it has pagination;
- allows grouping by multiple fields;
- almost fully customizable via CSS and "user space" render functions;
- export to Excel;
- export to graphics (pie-chart, bar-chart);
- especially designed for PostgreSQL;

Controller:

Code: Select all

 
class Report_Inbound_Call_Detailed extends General_Time_Report {
 
    function Report_Inbound_Call_Detailed()
    {
        parent::General_Report('inbound_call_detailed');
 
        $this->forms[] = 'filterform';
        $this->initForms();
 
        $this->timeform->with_hours = true;
        $this->filterform->fields = array(
            $this->report->fields[0], 
            $this->report->fields[1], 
            $this->report->fields[2], 
            $this->report->fields[3],
        );
    }
}
 
Model:

Code: Select all

 
class Report_Inbound_Call_Detailed_Model extends General_Time_Report_Model 
{
    function Report_Inbound_Call_Detailed_Model()
    {
        parent::General_Time_Report_Model();
        $this->with_totals = false;
    }
}
 
View: no view must be defined for 95% of my reports (exceptions are these reports who has crosstable queries).

In fact if I construct an object by calling

Code: Select all

new General_Report('inbound_call_detailed');
that would be enough to have a simple report without filtering and totals, but still with paging, sorting, Excel export.

So, the secret is in General_Time_Report_Model:

Code: Select all

 
    function get_report_fields_from_stored_procedure($procname)
    {
        $sql = "
            SELECT
                proargnames, proargmodes
            FROM
                pg_proc 
            WHERE
                pg_proc.proname = ?
            ";
............
       
;)

Also my translatation library is made in such way that would be automatically requested for translation in the UI of the user with 'translator' role ;)

Well, the project is for Intranet usage and is written for Mozilla Prism, so there might be many cross-browser issues ...
WOW, I'm so happy to boast about it :) but it really saved me a lot of time and work.
There are 10 types of people in this world, those who understand binary and those who don't
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: I figured out why I'll never be happy w/ php forms libraries

Post by Christopher »

Awesome guys.

Mordred, can you show us how your library would implement the Zend_Form example that I posted above. I am trying to get apples-to-apples comparison here. There is no way to see the strengths, weaknesses and interesting ideas unless each library implements the same solution. We can do more elaborate solution after this to showcase interesting features. As far as your scaffolding, besides the fact that I had no idea what the demo was showing, can you make it so it exports a PHP array or XML so other libs could read the data?

VladSun, obviously I want to know what General_Time_Report and General_Time_Report_Model look like. ;) Are they based on a General_Report and General_Model? How general purpose is your system. Even if it just did sub-totals and grand-totals for you it would be nice.
(#10850)
User avatar
Mordred
DevNet Resident
Posts: 1579
Joined: Sun Sep 03, 2006 5:19 am
Location: Sofia, Bulgaria

Re: I figured out why I'll never be happy w/ php forms libraries

Post by Mordred »

arborint wrote:Mordred, can you show us how your library would implement the Zend_Form example that I posted above. I am trying to get apples-to-apples comparison here. There is no way to see the strengths, weaknesses and interesting ideas unless each library implements the same solution. We can do more elaborate solution after this to showcase interesting features. As far as your scaffolding, besides the fact that I had no idea what the demo was showing, can you make it so it exports a PHP array or XML so other libs could read the data?
In your example code there wasn't any form handling - I'll show you mine if you show me yours ;) No really, how would they handle the form submission? I'm pasting all the relevant code from my demo. It shows form generation and submission handling.

http://library.localhost.com/libdemo/in ... mode=Login

Here's the code that generates the model, and a custom-written login form handler widget.

Code: Select all

class UsernameDescriptor extends StringDescriptor {
    function UsernameDescriptor($sType, $sDefault, $length) {
        StringDescriptor::StringDescriptor($sType, $sDefault, $length);
    }
    function IsValidInput($sValue, $bInsert) {
        $sValue = $this->AdjustInput($sValue);
        if (!StringDescriptor::IsValidInput($sValue, $bInsert)) return FALSE;
        if (!ctype_alnum($sValue)) return FALSE;
        if (!preg_match('~^[a-z]+~', $sValue)) return FALSE;
        return TRUE;
    }           
    function AdjustInput($sValue, $bForSearch = FALSE) {
        return strtolower($sValue);
    }
};
Descriptor::Register('username', 'UsernameDescriptor');
 
$g_pSql->m_pLogin =& new CTableSql('Login', 'Login');
$g_pSql->m_pLogin->AddColumn('m_nId', 'primary', true);
$g_pSql->m_pLogin->AddColumn('m_sUsername', 'username', false, Array(6, 20));
$g_pSql->m_pLogin->AddColumn('m_sPassword', 'password', false, Array(6, 255));
Here's how a custom descriptor does the equivalent of those validators and filters.

Here's a template file (I won't go into details on syntax. It's ugly 'cause it's generated) copied from the autogenerated templates and modified with custom texts and error messages:

Code: Select all

<h3>Login</h3>
<form method='post'><input type="hidden" name="mode" value="Login">
[[]][[bError_m_sUsername]]<span class='error'>[[/]]Username (cipherpunks): [[m_sUsername]][[]][[bError_m_sUsername]] (Username must be 6-20 alphanum)</span>[[/]]<br>
[[]][[bError_m_sPassword]]<span class='error'>[[/]]Password (writecode): [[m_sPassword]][[]][[bError_m_sPassword]] (Password must be longer than 6 chars)</span>[[/]]<br>
<input type='submit' name='do_submit' value='Login'></form>
And here's the widget with the custom controller and view functionality:

Code: Select all

class LoginWidget extends AutoEditLogin {
        var $m_bLogged = FALSE;
        function LoginWidget() {
            CWidget::CWidget('LoginWidget');
            
            $this->m_pLogin =& new Login();
            $this->m_pLogin->m_sUsername = 'cipherpunks';
            $this->m_pLogin->m_sPassword = 'write...what?';
            $this->CreateWidgets();
            
            if (isset($_REQUEST['do_submit'])) {
                $this->OnSubmit();
            }
        }
        function OnSubmit() {
            //actually here we should ask the model if it's a valid login.
            if ($this->m_pLogin->m_sUsername=='cipherpunks' && $this->m_pLogin->m_sPassword=='writecode')
                $this->m_bLogged = TRUE;
        }
        function ToString() {
            if ($this->m_bLogged) {
                return "Hello, {$this->m_pLogin->m_sUsername}";
            } else {
                $aErrors = Array();
                if (isset($_REQUEST[ 'do_submit' ])) {     
                    $aErrors = $this->m_pLogin->GetInputErrors( !$this->m_bEditOrAdd );
                }            
                $pTpl =& CreateTemplate('login.form.tpl.html');
                $pTpl->AssignObject($this);
                $pTpl->AssignArray($aErrors);
                return $pTpl->ToString();
            }
        }
    };
User avatar
inghamn
Forum Contributor
Posts: 174
Joined: Mon Apr 16, 2007 10:33 am
Location: Bloomington, IN, USA

Re: I figured out why I'll never be happy w/ php forms libraries

Post by inghamn »

Ugh, choosing a login as the example for the demo makes a challenge for me to provide the apples to apples comparison. My logins are not handled like any of the other forms...they're redirected to a login controller, instead of being handled by the same controller that displays the form view. And the model's authentication system is not nearly as simple as an update would be, since we support multiple authentication systems per application. With that said...here's what I think is the most relevant example

The view:

Code: Select all

 
<div id="login">
    <h1>Login</h1>
    <form method="post" action="<?php echo BASE_URL; ?>/login.php">
    <fieldset><legend>Login</legend>
        <table>
        <tr><td><label for="username">User:</label></td>
            <td><input name="username" id="username" size="10" /></td></tr>
        <tr><td><label for="password">Pass:</label></td>
            <td><input type="password" name="password" id="password" size="10" /></td></tr>
        </table>
        <button type="submit" class="login">Login</button>
    </fieldset>
    </form>
</div>
<script type="text/javascript">
    document.getElementById('username').focus();
</script>
 
An example controller that would display the login form.

Code: Select all

 
$template = new Template();
$template->blocks[] = new Block('loginForm.inc');
echo $template->render();
 
And the actual login controller..(Like I said, the authentication system in our stuff is fairly different other forms, since we've got a single login controller, that redirects back) Doing CRUD stuff would normally be handled by the same conroller that displayed the view.

Code: Select all

 
try
{
    $user = new User($_POST['username']);
 
    if ($user->authenticate($_POST['password'])) { $user->startNewSession(); }
    else { throw new Exception("wrongPassword"); }
}
catch (Exception $e)
{
    # You might want to redirect unsuccessful logins somewhere else
    $_SESSION['errorMessages'][] = $e;
    Header('Location: '.BASE_URL);
    exit();
}
 
# The user has successfully logged in.  Redirect them wherever you like
Header('Location: '.BASE_URL);
 

Choosing a CRUD operation would be a bit easier for me to provide and apples to apples example. Especially since our authentication system involves a dance of callback operations. Here's the method from the User class that would handle local, database authentication:

Code: Select all

 
    /**
     * Callback function from the SystemUser class
     * The SystemUser class will determine where the authentication
     * should occur.  If the user should be authenticated locally,
     * this function will be called.
     */
    protected function authenticateDatabase($password)
    {
        $PDO = Database::getConnection();
 
        $query = $PDO->prepare('select id from users where username=? and password=md5(?)');
        $query->execute(array($this->username,$password));
        $result = $query->fetchAll();
        return count($result) ? true : false;
    }
 
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: I figured out why I'll never be happy w/ php forms libraries

Post by Christopher »

Mordred wrote:In your example code there wasn't any form handling - I'll show you mine if you show me yours ;) No really, how would they handle the form submission? I'm pasting all the relevant code from my demo. It shows form generation and submission handling.
Well ... I had not gotten to that point yet. I had just posted Zend_Form code because jmut said it was the gold standard. I wanted some code that all of us could run. If I can't run the code then I can't really compare different solutions.
inghamn wrote:Ugh, choosing a login as the example for the demo makes a challenge for me to provide the apples to apples comparison. My logins are not handled like any of the other forms...they're redirected to a login controller, instead of being handled by the same controller that displays the form view. And the model's authentication system is not nearly as simple as an update would be, since we support multiple authentication systems per application. With that said...here's what I think is the most relevant example
Maybe login is not the best example. I actually do not care about the Authentication part -- that just distracts us from the forms code's design. What is a better example? How about something that has no database in its simplest version. A contact form that asks for name and email might be the simplest. That is a pretty common type of form. Later we could add code to save the data in a database to show how that would work.

To hopefully satisfy both of you, how about this modified example:

Code: Select all

<?php
include_once 'Zend/View.php';
include_once 'Zend/Form.php';
 
$form = new Zend_Form();
$form->setAction('')
     ->setMethod('post');
 
$fname = $form->createElement('text', 'fname', array('label' => 'First Name'));
$fname->addValidator('alpha')
         ->setRequired(true);
 
$lname = $form->createElement('text', 'lname', array('label' => 'Last Name'));
$lname->addValidator('alpha')
         ->setRequired(true);
         
// Create and configure password element:
$email = $form->createElement('text', 'email', array('label' => 'Email'));
$email->addValidator('EmailAddress')
         ->setRequired(true)
         ->addFilter('StringToLower');
         
// Add elements to form:
$form->addElement($fname)
     ->addElement($lname)
     ->addElement($email)
     // use addElement() as a factory to create 'Send' button:
     ->addElement('submit', 'send', array('label' => 'Send'));
 
if (!$form->isValid($_POST)) {
    echo $form->render(new Zend_View());
} else {
    echo '<pre>' . print_r($form->getValues(), 1) . '</pre>';
}
And the output:

Code: Select all

<form enctype="application/x-www-form-urlencoded" action="" method="post">
<dl class="zend_form">
<dt><label for="fname" class="required">First Name</label></dt>
<dd>
<input type="text" name="fname" id="fname" value="">
<ul class="errors"><li>Value is empty, but a non-empty value is required</li></ul>
</dd>
<dt><label for="lname" class="required">Last Name</label></dt>
<dd>
<input type="text" name="lname" id="lname" value="">
<ul class="errors"><li>Value is empty, but a non-empty value is required</li></ul>
</dd>
<dt><label for="email" class="required">Email</label></dt>
<dd>
<input type="text" name="email" id="email" value="">
<ul class="errors"><li>Value is empty, but a non-empty value is required</li></ul>
</dd>
<dt></dt>
<dd><input type="submit" name="send" id="send" value="Send"></dd>
</dl>
</form>
If you have the Zend framwork in the same directory as this script, or in the include path, you should be able to run it.
(#10850)
User avatar
inghamn
Forum Contributor
Posts: 174
Joined: Mon Apr 16, 2007 10:33 am
Location: Bloomington, IN, USA

Re: I figured out why I'll never be happy w/ php forms libraries

Post by inghamn »

Excellent example, I think I can provide a real apples to apples comparison.

Brief overview of the design....the model handles all input filtering and validation. The view escapes all the output. The controller assembles whatever views it wants into a template (aka Layout). The controller that uses a form is expected to handle what comes back when the form is POSTed.

You would run a genertator to create all of this. For this example, I only had to hand modify the filtering to match what arborint was doing to the input, and what happens on saving. Normally, you would save the object and redirect,

This code has been abbreviated from what's generated. Only what's necessary to match arborint's example has been left in.

Model

Code: Select all

 
class User
{
    private $firstname;
    private $lastname;
    private $email;
    
    public function save()
    {
        # Check for required fields before saving
        if (!$this->firstname || !$this->lastname)
        {
            throw new Exception("missingRequiredFields");
        }
    }
 
    public function getFirstname() { return $this->firstname; }
    public function getLastname() { return $this->lastname; }
    public function getEmail() { return $this->email; }
 
    public function setFirstname($string) { $this->firstname = preg_replace('/^a-zA-Z/','',trim($string)); }
    public function setLastname($string) { $this->lastname = preg_replace('/^a-zA-z/','',trim($string)); }
    public function setEmail($string) { $this->email = strtolower(trim($string)); }
}
 
Controller

Code: Select all

 
$user = new User();
if (isset($_POST['user']))
{
    foreach($_POST['user'] as $field=>$value)
    {
        $set = 'set'.ucfirst($field);
        $user->$set($value);
    }
    try
    {
        # Normally, here, we'd save the user and redirect 
        # But for now, we're just displaying the user's info
        $user->save();
        $block = new Block('users/userInfo.inc',array('user'=>$user));
    }
    catch(Exception $e) { $_SESSION['errorMessages'][] = $e; }
}
else
{
    $block = new Block('users/updateUserForm.inc',array('user'=>$user));
}
 
$template = new Template();
$template->blocks[] = $block;
echo $template->render();
 
Form View

Code: Select all

 
<form method="post" action="<?php echo $_SERVER['PHP_SELF']; ?>">
<fieldset><legend>Personal Info</legend>
    <table>
    <tr><td><label for="user-firstname">Firstname</label></td>
        <td><input name="user[firstname]" id="user-firstname" value="<?php echo View::escape($this->user->getFirstname()); ?>" /></td></tr>
    <tr><td><label for="user-lastname">Lastname</label></td>
        <td><input name="user[lastname]" id="user-lastname" value="<?php echo View::escape($this->user->getLastname()); ?>" /></td></tr>
    <tr><td><label for="user-email">Email</label></td>
        <td><input name="user[email]" id="user-email" value="<?php echo View::escape($this->user->getEmail()); ?>" />
    </table>
 
    <button type="submit" class="submit">Submit</button>
    <button type="button" class="cancel" onclick="document.location.href='<?php echo BASE_URL; ?>/users';">Cancel</button>
</fieldset>
</form>
 
Output View

Code: Select all

 
<table>
<tr><th>Firstname</th>
    <td><?php echo View::escape($this->user->getFirstname()); ?></td></tr>
<tr><th>Lastname</th>
    <td><?php echo View::escape($this->user->getLastname()); ?></td></tr>
<tr><th>Email</th>
    <td><?php echo View::escape($this->user->getEmail()); ?></td></tr>
</table>
 
What jumps out at me right away, is that my form handling stuff definitely assumes a model. And it assumes that you're going to want to save the model. I guess you could write the validation into the controller...but I really like it in the model. I may have several controllers that use the same model in different circumstances. It's nice to not have to keep writing the same validation code over and over again. The model seemed the best central place to put that.

I definitely don't use an object for forms, it always seems like too much work. To me a form is really just HTML. What gets passed to the controller is just input. There's ways to send stuff to a controller that don't involve HTML. This way, I can reuse the same controller to make it act like a service.
Post Reply