Forms Builder/Validation component [Feature]

1,323 views
Skip to first unread message

Romanko

unread,
Jan 11, 2013, 2:44:50 AM1/11/13
to pha...@googlegroups.com
There no link from blog page to Google Groups topic regarding forms builder and validator improvement, so I thought it would be useful to have one.

This is how I do it at work.

So, the first thing is a DataContainer. An instance of it is shared between form builder and validator and provides facility to access incoming data or extract parts or it. 

DataContainer receives data from GET or POST or php://input and keeps it internally. If data is multidimensional, such as an object or an array it can be accessed by referencing to a namespace & property name or by using an extractor method. Just to demonstrate it, let's consider we have a form where fields are organized in arrays:

<input type="text" name="user[firstname]" />
<input type="text" name="user[lastname]" />
<input type="text" name="user[email]" />

These fields would become an array: $_POST['user'] = array(
    'firstname' => 'John',
    'lastname' => 'Smith',
    'email' => 'john....@gmail.com'
);

Code to access these properties would look like:

$dc = new DataContainer($_POST);
$dc->getValue('user', 'fitrstname'); // using namespace "user" and property "firstname"
$dc->getValue('/user/firtstname'); // using XPath-like "/user/firstname"
$dc->getValue('fitrstname', function($data, $property) {
     return $data['user'][$property]
}); // using extractor function

$dc->extractValue('fitrstname', function($data, $property) {
     return $data['user'][$property]
}); // using extractor function

$dc->getValue('firstname'); // or 
$dc->firstname; // this will call extractor and return the value

  1. Extracted properties are cached internally, so when you do $dc->toArray() it will actually return array("firstname" => '...');
  2. If validator is a part of the form builder, changes on DataContainer become available to form builder itself, when we need to display the form again (e.g., for a user to correct errors)
---

Next thing is validator.

We assume that:

  • Data coming from user input must be transformed and / or validated. There can be zero-to-many transformations and / or validations required. And so, all filters performing transformations (sanitizing operations) and validators must be chained. For example, we "\n  john" and "smith     " as first and last names. We want transform them first and then validate. The chain will look like this:
$validator->createChain('firstname')
    ->trim() // trims value
    ->ucfirst() // capitalizes first letter
    ->is('alpha'); // validates
 
Magic __call captures "trim" and "ucfirst" and resolves then to rule class or PHP function to call.

$validator->createChain('lastname')->trim()->ucfirst()->is('alpha'); // once transformed it becomes "Smith"
  • While value advances through the chain, it may pass or fail. Failure can be
    • a soft failure, validation will fail but all rules will be applied to collect error messages from all validators
    • a hard failure, validation will stop on the chain
    • the stop signal, validation will stop $validator will terminate validation
The last one is useful when you rely on user input to work out the flow. E.g., you have a one form split into separate screens and you cannot show next screen until the first part of the form is not valid.

By default, all rules are hard rules, unless:

$validator->createChain('lastname')->softRule_is('alpha');
$validator->createChain('lastname')->stopRule_is('alpha');
$validator->createChain('lastname')->hardRule_isNot('digits'); // alias to $validator->...->isNot('digits');
  • Value can be required or optional. If optional it can be blank or contains some data and if required it can be something particular or exclusion.
$validator->createChain('credit-card')->is(array('visa', 'mastercard')); // invokes multiple validators, at least one must return TRUE
$validator->createChain('credit-card')->isNot('amex'); // assume we are OK to accept any card except AMEX
$validator->createChain('credit-card')->optional('cvv'); // accepts blank value or valid CVV \d{3,4}
  • DataContainer can be used inside validators to access other data. Let's say we collect area code and phone number separately. To validate phone number in international format, both parts should be put together:
$validator->createChain('phone-number')->transform(function($data) {
 return $data->getValue('area-code') . $data->getValue('phone-number');
})->is('phone');

I will add form builder part later. Have a nice weekend everyone! 

Andres Gutierrez

unread,
Jan 11, 2013, 2:02:23 PM1/11/13
to pha...@googlegroups.com
Thanks Romanko, this work makes the design of the component more clear to us, 

there are two things here, one is the Forms Builder (here we can re-use Phalcon\Tag to create the elements), the objectives of this component are:
  • Create forms in a more structured way
  • Apply validations to the entire form
  • Get and display validation messages on forms easily
So its basic usage can be:

class PostsController extends \Phalcon\Mvc\Controller
{
    protected function getForm()
    {
return $this->forms->createBuilder()
            ->setAction('posts/save')
            ->add('login', 'text')
            ->add('password', 'password')
            ->getForm();
     }

    public function createAction()
    {        
        $this->view->setVar('form', $this->getForm());
    }

Then in the view:

<!-- this will produce the whole form -->
<?php echo $form->render(); ?>

Or creating it with more free:

<form method="post" action="<?php echo $form->getAction() ?>">

<label for="login">Login</label> <?php echo $form->render("login"); ?>
<label for="password">Password</label> <?php echo $form->render("password"); ?>

<input type="submit" value="Send"/>

The save action receives the form data to perform the validation:

class SessionController extends \Phalcon\Mvc\Controller
{
    protected function getForm()
    {
return $this->forms->createBuilder()
            ->setAction('posts/save')
            ->add('login', 'text')
            ->add('password', 'password')
            ->getForm();
     }

    public function createAction()
    {        
        $this->view->setVar('form', $this->getForm());
    }
    
    public function saveAction()
    {
    $form = $this->getForm();
    
    //All fields are filled?
    if ($form->isValid($_POST) {
   
    $user = Users::findFirst(array(
    'login = ?0',
    'bind' => array($form->get('login'))
    ));
    if ($user) {    
    if ($this->security->checkHash($form->get('password'), $user->password) {
    $this->flash->success('Welcome ' . $user->name);
    return $this->dispatcher->forward(
    array('controller' => 'index')
    );
    }
    }
    }
   
    $this->view->setVar('form', $form);    
    }
}

Let's pretend the validation has failed (in the view):

<!-- Get all the messages -->
if ($form->hasMessages()) {
echo '<div class="messages">', $forms->messages(), '</div>';
}

<!-- Get messages for a field -->
<p>
<label for="login">Login</label> <?php echo $form->render("login"); ?>
<?php echo $form->messagesFor('login') ?>
</p>

The validator component is implicitly built inside the forms builder, also filters can be applied too:

protected function getForm()
    {
return $this->forms->createBuilder()
            ->setAction('posts/save')
            ->add('login', 'text', array(
            'label' => 'Le Login',
            'validators' => array(
            'MyApp\Validatiors\\Length' => array(
            'minimum' => 8,
            'message' => 'The login is not valid'
            )
            ),
            'filters' => array( //For Phalcon\Filter
            'alphanum'
            ),
            'attributes' => array( // For Phalcon\Tag::textField
            'size' => 40,
            'class' => 'input-btn'
            )
            ))
            ->add('password', 'password', array(
            'label' => 'Le Password',
            'attributes' => array(
            'size' => 40,
            'class' => 'input-btn'
            )
            ))
            ->getForm();
     }

2013/1/11 Romanko <roman....@gmail.com>

--
You received this message because you are subscribed to the Google Groups "Phalcon PHP Framework" group.
To post to this group, send email to pha...@googlegroups.com.
To unsubscribe from this group, send email to phalcon+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msg/phalcon/-/G0Uul_Ml9_IJ.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Romanko

unread,
Jan 11, 2013, 4:30:49 PM1/11/13
to pha...@googlegroups.com, andres.g...@phalconphp.com
Why do you want form builder to render a form? If you search for questions people ask on Zend_Form and forms in Symphony the good part of them are related to solving design issues / bad design possibilities when rendering forms, e.g., to use with Twitter Bootstrap or Mobile optimized forms.

I totally agree with the three options you listed below (and added a new one). In my opinion the core Form Builder should not cover all the possible use-cases, but it must be flexible enough for developer to extend it and build a new package on top, let's say to support Twitter Bootstrap or implement any other special case.

there are two things here, one is the Forms Builder (here we can re-use Phalcon\Tag to create the elements), the objectives of this component are:
  • Create forms in a more structured way
  • Apply validations to the entire form
  • Get and display validation messages on forms easily
  • Provide read-write access to element's data 
I am not trying to criticize, (no way!), but this below violates MVC rule of separation logic from presentation. Also, it becomes Yii style of declaring elements, which I really hate because it tends to use 1-meter-long, multidimensional, badly readable and hard-to-remember-all-the-parameters arrays :)

If you decouple it more and use OOP here it becomes lot easier and IDE friendly:

/**
 * Chained calls
 */

$this->forms->createBuilder()
    ->set{GET|POST}Action('posts/save')
    ->addElement('login') // no need to add element type, it must be defined in view template
    ->addFilter('blah')
    ->addValidator('dummy');

/**
 * Separate element configuration
 */

$element = $this->forms->addElement('password');
$element->addFilter('alphanum');

/**
 * Adding new element to the pool
 */

$this->forms->addElement( new Phalcon\Form\Element('remember_me', 'checked' /* value */ ); );

Andres Gutierrez

unread,
Jan 12, 2013, 3:17:38 PM1/12/13
to pha...@googlegroups.com
Check out this gist: https://gist.github.com/4520220

This consolidates some of the ideas proposed + the observations of Romanko, 
  • The form receives a model and uses it to display the data in the appropriate fields in the edit form
  • Adding elements to the form is now decoupled, same with the validators
  • The issues to be discussed now are that the ORM validators are now duplicating the possible validators to  be introduced in the Validation component. ¿?
  • The forms validations would pass but the ones in the model not, so possibly the developers need to append the messages produced in the model to the form to properly show them on the view 
2013/1/11 Romanko <roman....@gmail.com>
--
You received this message because you are subscribed to the Google Groups "Phalcon PHP Framework" group.
To post to this group, send email to pha...@googlegroups.com.
To unsubscribe from this group, send email to phalcon+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msg/phalcon/-/8Hn2QrzegycJ.

Todor

unread,
Jan 12, 2013, 3:52:16 PM1/12/13
to pha...@googlegroups.com, andres.g...@phalconphp.com
Hi,

I like it :)
It is clear ans easy to understand. Still I want to mention several other form libraries I have been using and compare them with yours:

You can do everything you can think of. All the form is just an array - simple, powerful (and helplessly slow). 
Easy to use and you apply it directly in the view which I find more natural (you don't generate the form as a whole but each element separately and you can put them in as many divs and other design elements as you want). Other quite important feature of this library is that it adds not only PHP but also JavaScript validation.
3. Ruby of Rails + SimpleForm - the best and most beautiful and natural solution. Again the form is not generated as a whole but every form element is defined separately in the view.

To say it simple: 
1. I think it's better the form to be defined in the view and not as a whole in a separate class so the designers will have more freedom to style it. Also If I want to use Phalcon as a base for CMS there should be a way for creating dynamically generated form and validation. (the same is true for the routing but we are not talking about it here)
2. There should be not just PHP but also JavaScript validation (when a validation rule is set) but also there sould be a way to switch it off if needed.

I hope that this information was useful.
I wish you success and I think you are doing something unique and amazing,
Todor Pavlov.

Romanko

unread,
Jan 13, 2013, 8:39:53 PM1/13/13
to pha...@googlegroups.com, andres.g...@phalconphp.com
1. I think it's better the form to be defined in the view and not as a whole in a separate class so the designers will have more freedom to style it. Also If I want to use Phalcon as a base for CMS there should be a way for creating dynamically generated form and validation. (the same is true for the routing but we are not talking about it here)

Framework != CMS :) Framework is just a set of tools to solve common problems when building a website. CMS is a framework in a cool package and this package is specially designed for an average user to start clicking through buttons to bring up an average website with no programming hassle.
 
2. There should be not just PHP but also JavaScript validation (when a validation rule is set) but also there sould be a way to switch it off if needed

See above, anything related to client-side stuff (JavaScript libraries, CSS frameworks etc) must come as pre-packaged, they must be chosen the developer instead and blended into the app.

Romanko

unread,
Jan 13, 2013, 8:59:05 PM1/13/13
to pha...@googlegroups.com, andres.g...@phalconphp.com
  • The form receives a model and uses it to display the data in the appropriate fields in the edit form
  • Adding elements to the form is now decoupled, same with the validators
    Let's abstract form elements:
    • each element has a name, this how we identify it inside form element and access it
    • each element has a value
    • each element may have an array of pre-defined values (e.g, radio button group or <select />)
    • each element has an optional set of attributes
    The latter option is what you 

    $title->setAttributes([
    'class' => 'input-btn',
    'size' => 40
    ]);
     
    It seems wrong, because Controller is to invoke user actions and View is to present data to the user. Assigning view data to form elements inside controller breaks the "separation of concerns" concept and requires coding for presentation layer where it must not happen.

    For the sake of flexibility rendering should happen in different way:

    • Plain HTML: <input type="text" name="<?= $form->getElement('username')->name; ?>" value="<?= $form->getElement('username')->value; ?>">
    • Using helper: Phalcon\Tag::textField($form->getElement('username')->name, $form->getElement('username')->value[, attributes])
    • $form->render("username", array([ attributes ])); // re-uses Phalcon\Tag
    If it is done so, people will so no problems creating wrappers for Twitter Bootstrap, jQuery UI and other frameworks.

    Next, considering that form elements accept and return scalar values, there is no logical difference between TextElement, TextAreaElement or whatever element you define. They all operate with 4 options I listed above. I think form element should be a simply Element.

    Next, Validator can be standalone component and used by the form builder as well by the ORM. It solves your concern:
    • The issues to be discussed now are that the ORM validators are now duplicating the possible validators to  be introduced in the Validation component. ¿?
    • The forms validations would pass but the ones in the model not, so possibly the developers need to append the messages produced in the model to the form to properly show them on the view  
    Next, the CRUD thing: when performing CREATE and UPDATE operations using ORM and data from the form builder, ORM validation can be disabled, because data from the form builder already comes clean. The same can apply to READ & DELETE operations, but if form builder is not used here, ORM validation should take over data pre-processing.

    I can prepare code samples later if you'd like.

    Cheers!

    Alex

    unread,
    Jan 14, 2013, 1:46:20 PM1/14/13
    to pha...@googlegroups.com, andres.g...@phalconphp.com
    there is code from from the gist:
    ->addValidation(new PresenceOf([
    'message' => 'The title is required'
    ])
    ->addFilters(
    ['striptags', 'upper']
    );

    Why there is difference in the way how you pass validators and filters? I think they have to have same syntax:
    ->addValidation(new PresenceOf(array(
    'message' => 'The title is required'
    ))
    ->addValidation(new MyValidator(array(
    'message' => 'The title is required'
    ))
    ->addFilter(new StringTags());
    ->addFilter(new Upper());
    or
    ->addValidation('PresenceOf',(array(
    'message' => 'The title is required'
    ))
    ->addValidation('MyValidator',array(
    'message' => 'The title is required'
    ))
    ->addFilter('StringTags');
    ->addFilter('Upper');


    You said : The form receives a model and uses it to display the data in the appropriate fields in the edit form.

    Why form should be linked to the model, what id there is no models in the project? I think form should me completely independent   from models. 
    So, they how it should work, you validate  your data in the form and then pass to model or whenever you need.

    I agree with Romanko about how form should be separate from the view. If you want to assign css classes to the form elements in the form itself it better to attach some kind of decorator helper to element or form. This approach will give more flexibility in terms of what you can add to the form element e.g. attributes or wrap element into some html tags, etc..

    Nikolaos Dimopoulos

    unread,
    Jan 14, 2013, 1:58:37 PM1/14/13
    to pha...@googlegroups.com
    It would make sense to me if it was actually:

    
    
    ->addFilters(array('StringTags', 'Upper'));

    The contents of this message may contain confidential or privileged information and is intended solely for the recipient(s). Use or distribution to and by any other party is not authorized. If you are not the intended recipient, copying, distribution or use of the contents of this information is prohibited.


    --
    You received this message because you are subscribed to the Google Groups "Phalcon PHP Framework" group.
    To post to this group, send email to pha...@googlegroups.com.
    To unsubscribe from this group, send email to phalcon+u...@googlegroups.com.
    To view this discussion on the web visit https://groups.google.com/d/msg/phalcon/-/PipovRLOR3gJ.

    Alex

    unread,
    Jan 14, 2013, 2:16:54 PM1/14/13
    to pha...@googlegroups.com
    In that case how do you passing additional parameters into each filter?
    It works fine for simple filters like trim, but what if you need to build more advanced filter?

    Nilton Souza

    unread,
    Jan 14, 2013, 2:18:33 PM1/14/13
    to pha...@googlegroups.com
    +1

    Nikolaos Dimopoulos

    unread,
    Jan 14, 2013, 2:19:42 PM1/14/13
    to pha...@googlegroups.com
    I am _guessing_ that the case would be exactly as you described it. For filters that are part of the library such as the Upper, StripTags you could use that kind of definition.

    As far as custom ones, you will need to use

    ->addFilter(new Filtername(params));

    or

    ->addFilter(array(new FilterOne(params), new FilterTwo(params)));

    But this is just me thinking out loud.

    The contents of this message may contain confidential or privileged information and is intended solely for the recipient(s). Use or distribution to and by any other party is not authorized. If you are not the intended recipient, copying, distribution or use of the contents of this information is prohibited.


    To view this discussion on the web visit https://groups.google.com/d/msg/phalcon/-/cEyurJLlk8wJ.

    Andres Gutierrez

    unread,
    Jan 14, 2013, 2:33:37 PM1/14/13
    to pha...@googlegroups.com
    The filters are designed that way because they're the strings passed to Phalcon\Filter: http://docs.phalconphp.com/en/latest/reference/filter.html#sanitizing-data

    More filters can be globally registered by the developer: http://docs.phalconphp.com/en/latest/reference/filter.html#creating-your-own-filters

    2013/1/14 Alex <carme...@gmail.com>
    --
    You received this message because you are subscribed to the Google Groups "Phalcon PHP Framework" group.
    To post to this group, send email to pha...@googlegroups.com.
    To unsubscribe from this group, send email to phalcon+u...@googlegroups.com.
    To view this discussion on the web visit https://groups.google.com/d/msg/phalcon/-/PipovRLOR3gJ.

    Andres Gutierrez

    unread,
    Jan 14, 2013, 2:50:40 PM1/14/13
    to pha...@googlegroups.com
    I'm using a model as example, it could be any abstract type (any class that implements setters/getters or public properties) this is used by the form to get the default values to be displayed in the form when it's rendered in edit mode also retrieve the final filtered/validated values and assign it again to the abstract type or model to finally perform the storing of the data.

    Regarding the 'attributes',  I'm looking for a way to reuse CSS class names/HTML attributes across forms, I'm assuming in that case that the app would have themes or it is using twitter bootstrap so I want to render all the text-fields with a common style or certain forms with common styles.

    This not necessarily need to be part of the specification of the form, then perhaps make clear that the form will not be aware of aspects of visualization/presentation and this must be implemented by the developer on his/her responsibility.

    I don't know if the validation layer in the ORM can be completely disabled, there are validators like Uniqueness, Virtual Foreign Keys, business rules of each model, that cannot be implemented as Validators. Those messages produced by the model also need to be shown to the user in the similar way as normal messages are shown.

    2013/1/13 Romanko <roman....@gmail.com>

    --
    You received this message because you are subscribed to the Google Groups "Phalcon PHP Framework" group.
    To post to this group, send email to pha...@googlegroups.com.
    To unsubscribe from this group, send email to phalcon+u...@googlegroups.com.
    To view this discussion on the web visit https://groups.google.com/d/msg/phalcon/-/frwS5RaOoDsJ.
    Reply all
    Reply to author
    Forward
    0 new messages