Get up to 80 % extra points for free! More info:

Lesson 11 - Finishing the FormControl class in PHP

In the previous lesson, FormControl - Parent of form controls in PHP, we started working on the FormControl class which serves as a parent of all form controls. In today's tutorial, we're going to finish this class.

Since we've already added the addPatternRule() method, we can check regular expressions in other rules. We'll use it for things such as the minimal-length rule:

Minimal-length rule

public function addMinLengthRule($minLength, $validateClient = true, $validateServer = true)
{
    return $this->addPatternRule('.{' . $minLength . ',}', $validateClient, $validateServer);
}

Password rule

Next, we'll add a rule which validates passwords. The password will have to be at least 6 characters long (we'll implement it using the pattern rule). Also, the password will not be able to contain accent characters, which we'll only check on the server-side so as to keep things simple.

public function addPasswordRule($validateClient = true, $validateServer = true)
{
    $this->addMinLengthRule(6, $validateClient);
    return $this->addRule(array(
        'type' => self::RULE_PASSWORD,
        'message' => 'The password cannot contain accent characters and has to be at least 6 characters long.',
    ), $validateClient, $validateServer);
}

Date-time rule

Now, let's add several methods which deal with rules regarding date and time (date and time, date only, and time only). We'll add a pattern for each rule.

public function addDateTimeRule($validateClient = true, $validateServer = true)
{
    $this->addPatternRule('[0-1]?[0-9]/[0-3]?[0-9]/[0-9]{4}\s[0-2]?[0-9]\:[0-5]?[0-9](\:[0-5]?[0-9])?');
    return $this->addRule(array(
        'type' => self::RULE_DATETIME,
        'format' => DateUtils::DATETIME_FORMAT,
        'message' => 'The value must match the following format: mm/dd/yyyy hh:mm(:ss)',
    ), $validateClient, $validateServer);
}

public function addDateRule($validateClient = true, $validateServer = true)
{
    $this->addPatternRule('[0-1]?[0-9]/[0-3]?[0-9]\.[0-9]{4}');
    return $this->addRule(array(
        'type' => self::RULE_DATETIME,
        'format' => DateUtils::DATE_FORMAT,
        'message' => 'The value must match the following format: mm/dd/yyyy',
    ), $validateClient, $validateServer);
}

public function addTimeRule($validateClient = true, $validateServer = true)
{
    $this->addPatternRule('[0-2]?[0-9]\:[0-5]?[0-9](\:[0-5]?[0-9])?');
    return $this->addRule(array(
        'type' => self::RULE_DATETIME,
        'format' => DateUtils::TIME_FORMAT,
        'message' => 'The value must match the following format: hh:mm(:ss)',
    ), $validateClient, $validateServer);
}

Required-file rule

Great! Now, go ahead and add the last method for the required-file rule.

public function addFileRequiredRule($validateClient = true, $validateServer = true)
{
    return $this->addRule(array(
        'type' => self::RULE_REQUIRED_FILE,
        'message' => 'You have to attach a file',
    ), $validateClient, $validateServer);
}

Surely, we could come up with many another rules. However, I've always been able to get the job done by combining the rules we just now set up. If anything is truly missing in the future, all we'd have to do is come back and add a new rule. Anyway, we should definitely keep the number of rules at a bare minimum since we'll derive additional rules from these in the Form class.

Validation

Now, let's move on to adding rules to the control. Let's implement the part of the class which will evaluate these rules. As you already know, we'll have to do this twice - on the client-side and on the server-side.

The client part

The method below adds HTML attributes to a control based on the rules it contains. We're using HTML 5 attributes, so we are able to evaluate regular expressions, required fields, and maximal lengths very easily.

public function addClientParams()
{
    foreach ($this->rules as $rule)
    {
        if ($rule['validate_client'])
        {
            switch ($rule['type'])
            {
                case self::RULE_REQUIRED:
                case self::RULE_REQUIRED_FILE:
                    $this->htmlParams['required'] = 'required';
                    break;
                case self::RULE_MAX_LENGTH:
                    $this->htmlParams['maxlength'] = $rule['max_length'];
                    break;
                case self::RULE_PATTERN:
                    if (!isset($this->htmlParams['pattern']))
                        $this->htmlParams['pattern'] = $rule['pattern'];
                    break;
            }
        }
    }
}

Unfortunately, HTML 5 doesn't support multiple patterns, so only the first pattern will be evaluated on the client-side. If there are any more patterns, which hasn't happened to me up until now, they'll only be evaluated on the server-side.

The server part

We'll add a similar switch into the checkRule() method as well, which evaluates a rule on the server. Notice the use of the DateUtils and StringUtils libraries which we made earlier on in the course.

private function checkRule($rule)
{
    $name = $this->name;
    switch ($rule['type'])
    {
        case self::RULE_REQUIRED:
            return isset($_POST[$name]) && (is_numeric($_POST[$name]) || !empty($_POST[$name]));
        case self::RULE_MAX_LENGTH:
            return !isset($_POST[$name]) || !$_POST[$name] || mb_strlen($_POST[$name]) <= $rule['max_length'];
        case self::RULE_PATTERN:
            return !isset($_POST[$name]) || !$_POST[$name] || preg_match('~^' . $rule['pattern'] . '$~u', $_POST[$name]);
        case self::RULE_REQUIRED_FILE:
            return isset($_FILES[$name]) && isset($_FILES[$name]['name']) && $_FILES[$name]['name'];
        case self::RULE_DATETIME:
            return !isset($_POST[$name]) || !$_POST[$name] || DateUtils::validDate($_POST[$name], $rule['format']);
        case self::RULE_PASSWORD:
            return !isset($_POST[$name]) || !$_POST[$name] || ((StringUtils::removeAccents($_POST[$name]) == $_POST[$name]) && (mb_strlen($_POST[$name]) >= 6));
    }
    return false;
}

If any of rules the don't apply, the control will be highlighted in red. Add the public $invalid property to the class:

public $invalid;

We'll finish up with the validations by adding a supreme checkValidity() method, which evaluates all of the rules. If any of the rules don't apply, it will set the $invalid variable to true, add an "invalid" css class, and throw an exception:

public function checkValidity()
{
    foreach ($this->rules as $rule)
    {
        if (($rule['validate_server']) && (!$this->checkRule($rule)))
        {
            $this->invalid = true;
            $this->addClass('invalid');
            throw new UserException($rule['message']);
        }
    }
}

We'll use the UserException class for every exception where a message is meant to be displayed to the user:

class UserException extends Exception
{

}

Rendering

We'll render (generate HTML code for a control) content using an abstract method which will later be implemented by a concrete control. We'll also have the client validating parameters added to the control when they're being rendered. For this reason, we'll keep the descendant method marked as protected and call it from the public render() method:

protected abstract function renderControl($isPostBack);

public function render($validateClient, $isPostBack)
{
    if ($validateClient)
        $this->addClientParams();
    return $this->renderControl($isPostBack);
}

Retrieving data and filling data in

We still have to add an interface which retrieves a control's data and fills the data back in. We'll also have to consider the fact that a single control can contain multiple values. This way, we can implement CheckLists, i.e. a control with multiple CheckBoxes. The main advantage to having multiple fields in a single control is easier form definition.

Let's go ahead and add the getData() method which will return the data in a control. If a value has been submitted to the server, there is a key with a control name in $_POST, we'll return the value from said location. Otherwise, we won't return anything. In order to be able to return multiple values, we'll set it up so that it always returns the data as an array. Controls containing multiple fields will then override this method, then, the controls that return a single value will keep it.

public function getData()
{
    return isset($_POST[$this->name]) ? array($this->name => $_POST[$this->name]) : array();
}

Let's add a method which returns the keys for all of the fields in a control in a similar way:

public function getKeys()
{
    return array($this->name);
}

The function which sets the value will depend on a concrete control. Therefore, we'll define it as abstract:

public abstract function setData($key, $value);

We are now done with the base for our controls. In the next lesson, Form framework for PHP - InputBox, we'll implement our first control, an InputBox. The finished and documented FormControl class is available for download in the attachment below.


 

Did you have a problem with anything? Download the sample application below and compare it with your project, you will find the error easily.

Download

By downloading the following file, you agree to the license terms

Downloaded 15x (12.12 kB)
Application includes source codes in language PHP

 

Previous article
FormControl - Parent of form controls in PHP
All articles in this section
Libraries for PHP
Skip article
(not recommended)
Form framework for PHP - InputBox
Article has been written for you by David Capka Hartinger
Avatar
User rating:
1 votes
The author is a programmer, who likes web technologies and being the lead/chief article writer at ICT.social. He shares his knowledge with the community and is always looking to improve. He believes that anyone can do what they set their mind to.
Unicorn university David learned IT at the Unicorn University - a prestigious college providing education on IT and economics.
Activities