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