Автоматизация создания и валидации форм

в 8:51, , рубрики: php, web-разработка, разработка, метки: ,

Предисловие

Создание форм — один из самых ответственных процессов при создании любого сайта. Эта часть сайта, как правило, в большей степени требует пристального внимание, чтобы предотвратить различные неприятные ситуаций. Для пользователей — это самый главный способ передачи информации на сервер, при котором нужно контролировать передаваемые значения. Наверное, 99% всех сайтов содержат формы, поэтому надеюсь, что статья сможет облегчить жизнь, как минимум, начинающим web-программистам.
Топик не является руководством к действию и не призывает всегда использовать данную технологию создания форм и работы с ними. Статья рассчитана главным образом на программистов, которые до сих пор пишут формы и код валидации для каждого поля вручную.

Итак, формы

<form name="loginform" action="/login" method="post" class="formclass" autocomplete="off">
...
<form>

Содержит ряд свойств и состоит из различных элементов, с помощью которых организуется ввод данных различного формата. Каждая форма имеет различные свойства, различное чисто элементов и их разнообразие. Каждый раз, код должен контролировать корректность ввода ещё до отправки на сервер, чтобы пользователь не ждал ответа от сервера сообщения, которое пользователь может получить ещё до отправки данных на сервер. Это и частично снижает нагрузку на сам сервер и уменьшает время, которое пользователь тратит на ожидание ответа. В свою очередь необходимо дублировать проверку и на самом сервере, потому что пришедшим от клиента данным нельзя доверять.

Вписывание в существующий движок сайта

Приведённые в статье примеры незначительно адаптированы под архитектуру MVC, но вы с лёгкостью можете подправить классы под свою архитектуру. Цель статьи — донести общий принцип, а не показать как нужно писать код.
Статичная часть HTML-разметки находится в шаблонах. На первый взгляд напрашивается вариант прописать формы в файлах шаблона. Но что если на вашем сайте предусмотрено большое количество форм? Да, можно потратить несколько дней, недельку, прописать все формы в шаблонах, далее создать JavaScript-код, который бы валидировал формы на стороне клиента перед отправкой, далее создать проверку каждого значения на стороне сервера и т.д. И всё это для каждой формы! Давайте посмотрим, как этот процесс можно оптимизировать и значительно облегчить себе задачу. Оформим всё в виде классов и в дальнейшем легко сможем использовать в последующих своих проектах.

Создадим в удобном месте своего сайта директорию forms и в неё будем складывать файлы с описанием форм. Я использую для этого XML. Предлагаемый формат файла формы:

<?xml version="1.0" encoding="utf-8"?>
<validator method="post" action="/json/login" class="form">
<field type="word" name="username" req="true" value="" min="4" max="25">User name</field>
<field type="password" name="password" req="true" value="" min="4" max="25">User password</field>
<field type="submit">Log In</field>
</validator>

По описанию видно, что форма, состоящая из трёх элементов, должна отправиться методом POST на относительный адрес /json/login и имеет CSS-класс form.
Элементы формы:

  • Поле для ввода имени пользователя, с ограничением по минимальной и максимальной длины вводимого текста
  • Поле для ввода пароля, с ограничением по минимальной и максимальной длины вводимого текста
  • Кнопка для отправки формы

Главным атрибутом полей является TYPE, который однозначно идентифицирует, какой HTML-элемент будет создан на странице и какие значения в него можно будет вводить. Также будет автоматически сгенерирован код JavaScript для валидации обязательных полей формы на стороне клиента, а также на стороне сервера после принятия значений формы.

Вставка формы в web-страницу

Для автоматической вставки формы на страницу, в шаблоне в нужном месте вставим метку <!--[LOGIN]-->, далее в шаблонизаторе необходимо реализовать метод, который бы вставлял вместо метки форму. У меня это сделано так:

public function setForm($data)
   {
    $p1='/<!--[(w+)]-->/';
    $n =preg_match_all($p1, $data, $matches);
    If ($n>0)
    { 
      require_once(PATH."formClass.php");
      for ($i=0; $i<=$n-1; $i++)
 	{
 	 $p2='/<!--['.$matches[1][$i].']-->/';
         $form = new formClass(strtolower($matches[1][$i]));
 	 $data = preg_replace($p2, $form->getHTML(), $data);
 	}
    }
    return $data;
   }

Параметр $data — это текст шаблона.

Создание формы

Форма состоит из разнообразных элементов, у каждых элементов есть общие обязательные свойства, поэтому напрашивается использование базового класса для всех элементов с последующим наследованием.

class baseHTMLelement
{
    protected $title, $name, $req, $min=0, $max=255, $value='', $js;
    
    public function __construct($name, $title, $req='false')
    {
        $this->name     = $name;
        $this->title    = $title;
        if ($req == 'true')
        {
            $this->req = true;
        }
        else
        {
            $this->req = false;
        }
    }
    
    public function freq($req)
    {
        if ($req == 'true')
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    
    public function add_allow_char($patern)
    {
        $this->js .= 'if(el.name=="'.$this->name.'"){var Pattern=/^(['.$patern.']+)$/i;if(!Pattern.test(value)){showmessage("['.$this->title.']: The field can only contain <em>'.$patern.'</em>");break;}}';
    }
    
    public function add_rule($rule)
    {
        $this->js .= $rule;
    }
    
    public function add_regex($patern, $messege)
    {
        $this->js .= 'if(el.name=="'.$this->name.'"){var Pattern=/'.$patern.'/i; if (!Pattern.test(value)){showmessage("['.$this->title.']: '.$messege.'");break;}}';
    }
    
    public function setValue($value='')
    {
        $this->value = $value;
    }
    
    public function get_title()
    {
        return $this->title;
    }
    
    public function is_req()
    {
        return $this->req;
    }
    
    public function get_js()
    {
        if ($this->req == false)
        {
            return '';
        }
        else
        {
            return 'if((el.name=="'.$this->name.'")&(value=="")){showmessage("['.$this->title.']: The field can not be empty");break;}'.
            'if((el.name=="'.$this->name.'")&(value.length<'.$this->min.')){showmessage("['.$this->title.']: Length should not be less than '.$this->min.'");break;}'.$this->js;
        }
    }
    
    public function setLenght($min=0, $max=255)
    {
        $this->min = $min;
        $this->max = $max;
    }
}

Этот класс создаёт элемент с необходимыми для каждого элемента свойствами, а также обеспечивает генерацию JavaScript-кода, обеспечивающего контроль заполненности поля.

Далее создаём ещё удин уровень абстракции — все поля ввода INPUT:

class inputHTMLelement extends baseHTMLelement
{
    public function __construct($name, $title, $req = 'false')
    {
        parent::__construct($name, $title, $req);
    }

    public function get_html()
    {
        return '<input type="text" name="'.$this->name.'" value="'.$this->value.'" maxlength="'.$this->max.'">';
    }
}

Для поля ввода пароля создадим отдельный класс, где переопределим метод get_html, отвечающий за HTML-код элемента:

class inputPasswordHTMLelement extends inputHTMLelement
{
    public function __construct($name, $title, $req = 'false')
    {
        parent::__construct($name, $title, $req);
    }
    
    public function get_html()
    {
        return '<input type="password" name="'.$this->name.'" value="'.$this->value.'" maxlength="'.$this->max.'">';
    }
}

Ну и, наконец, для кнопки:

class submitHTMLelement extends baseHTMLelement
{
    protected $value;
    public function __construct($value)
    {
        parent::__construct('submit', $title='');
        $this->value = $value;
    }
    
    public function get_html()
    {
        return '<button onclick="checkForm();return false;">'.$this->value.'</button>';
    }
}

Вы спросите почему кнопка именно такая? Я использую AJAX для отправки форм, поэтому мне удобен такой вариант, вы у себя можете сделать так, как вам удобно.
Остальные элементы формы попробуйте написать сами по такому же принципу.

Базовая форма

По аналогии с элементами, формы тоже имеют набор обязательных для всех форм свойствами, поэтому я решил создать базовый класс для всех создаваемых форм:

class baseHTMLform
{
    protected $name, $action, $method, $class, $autocomplete;
    protected $elements = array();
    
    public function __construct($name, $action, $method, $class, $autocomplete=false)
    {
        $this->name     = $name;
        $this->action   = $action;
        $this->method   = $method;
        $this->class    = $class;
        If ($autocomplete == true)
        {
            $this->autocomplete = "on";
        }
        else
        {
            $this->autocomplete = "off";
        }
    }
    
    public function addElement($el)
    {
        $this->elements[] = $el;
    }
    
    public function show()
    {
        $html = '<div id="errortext"></div><form name="'.$this->name.'" action="'.$this->action.'" method="'.$this->method.'" class="'.$this->class.'" autocomplete="'.$this->autocomplete.'"><table>';
        $js = '<script type="text/javascript" src="/js/api.js?rand=0.01"></script>';
        $js.= '<script type="text/javascript">function checkForm(){var el,elName,value,type,form;error=0;form=document.forms["'.$this->name.'"];for(var i = 0;i<form.length;i++){el=form.elements[i];elName=el.nodeName.toLowerCase();value=$.trim(el.value);';
        foreach ($this->elements as $el)
        {
            $reqstar = '';
            If ($el->is_req())
            {
                $reqstar = '<em class="red">*</em>';
            }
            $js .= $el->get_js();
            $html .= '<tr><td>'.$el->get_title().$reqstar.'</td><td>'.$el->get_html().'</td></tr>';
        }
        $html .= '</table></form>';
        $js .= '}if(error == 0){post("'.$this->name.'", "'.$this->action.'", "#errortext");}}</script>';
        return $js.$html;
    }
}

Класс реализует методы, создающие HTML-разметку формы и генерирующие валидационный JavaScript-код.
В листринге указан прикрепляемый файл api.js, это моя собственная библиотека, с помощью которой я по AJAX передаю форму на сервер и принимаю результат, а также реализовано описание таких функций как showmessage.

Сборка формы из элементов и создание правил для вводимых значений

class formClass
{
    private $formfile, $name;
    
    public function __construct($name)
    {
        $this->formfile = '../application/forms/'.$name.'.xml';
        $this->name     = $name;
        if(!is_file($this->formfile)) new CustomException('Not found form: "'.$name.'"');
    }
    
    public function getHTML()
    {
        $xml   = simplexml_load_file($this->formfile);
        $form = new baseHTMLform($this->name, $xml['action'], $xml['method'], $xml['class']);
        $html = '';
  
        foreach ($xml->field as $element)
        {
            switch ($element['type'])
            {
                case "text":
                    $htmlElement = new inputHTMLelement($element['name'], $element, $element['req']);
                    $htmlElement->setLenght($element['min'], $element['max']);
                    $htmlElement->setValue($element['value']);
                    $form->addElement($htmlElement);
                    break;
                case "password":
                    $htmlElement = new inputPasswordHTMLelement($element['name'], $element, $element['req']);
                    $htmlElement->setLenght($element['min'], $element['max']);
                    $htmlElement->setValue($element['value']);
                    $form->addElement($htmlElement);
                    break;
                case "word":
                    $htmlElement = new inputHTMLelement($element['name'], $element, $element['req']);
                    $htmlElement->setLenght($element['min'], $element['max']);
                    $htmlElement->setValue($element['value']);
                    $htmlElement->add_allow_char('-_0-9a-zA-Zа-яА-Я');
                    $form->addElement($htmlElement);
                    break;
                case "submit":
                    $htmlElement = new submitHTMLelement($element);
                    $form->addElement($htmlElement);
                    break;
                case "alpha":
                    $htmlElement = new inputHTMLelement($element['name'], $element, $element['req']);
                    $htmlElement->setLenght($element['min'], $element['max']);
                    $htmlElement->setValue($element['value']);
                    $htmlElement->add_allow_char('a-zA-Zа-яА-Я');
                    $form->addElement($htmlElement);
                    break;
                case "alpha-number":
                    $htmlElement = new inputHTMLelement($element['name'], $element, $element['req']);
                    $htmlElement->setLenght($element['min'], $element['max']);
                    $htmlElement->setValue($element['value']);
                    $htmlElement->add_allow_char('0-9a-zA-Zа-яА-Я');
                    $form->addElement($htmlElement);
                    break;
                case "number-int":
                    $htmlElement = new inputHTMLelement($element['name'], $element, $element['req']);
                    $htmlElement->setLenght($element['min'], $element['max']);
                    $htmlElement->setValue($element['value']);
                    $htmlElement->add_allow_char('0-9');
                    $form->addElement($htmlElement);
                    break;
                case "time":
                    $htmlElement = new inputHTMLelement($element['name'], $element, $element['req']);
                    $htmlElement->setLenght($element['min'], $element['max']);
                    $htmlElement->setValue($element['value']);
                    $htmlElement->add_allow_char('-: 0-9 a-zA-Zа-яА-Я');
                    $form->addElement($htmlElement);
                    break;
                case "phone":
                    $htmlElement = new inputHTMLelement($element['name'], $element, $element['req']);
                    $htmlElement->setLenght($element['min'], $element['max']);
                    $htmlElement->setValue($element['value']);
                    $htmlElement->add_allow_char('-()+ 0-9');
                    $form->addElement($htmlElement);
                    break;
                case "strings-alpha":
                    $htmlElement = new inputHTMLelement($element['name'], $element, $element['req']);
                    $htmlElement->setLenght($element['min'], $element['max']);
                    $htmlElement->setValue($element['value']);
                    $htmlElement->add_allow_char('/-a-zA-Zа-яА-Я, 0-9');
                    $form->addElement($htmlElement);
                    break;
                case "repassword":
                    $htmlElement = new inputPasswordHTMLelement($element['name'], $element, $element['req']);
                    $htmlElement->setLenght($element['min'], $element['max']);
                    $htmlElement->add_rule('if((el.name=="'.$element['name'].'")&(elName=="input")&(value!=form.password.value)){showmessage("['.$element.']: Password does not match");break;}');
                    $form->addElement($htmlElement);
                    break;
                case "email":
                    $htmlElement = new inputHTMLelement($element['name'], $element, $element['req']);
                    $htmlElement->setLenght($element['min'], $element['max']);
                    $htmlElement->setValue($element['value']);
                    $htmlElement->add_regex('^([a-z0-9+_-]+)(.[a-z0-9+_-]+)*@([a-z0-9-]+.)+[a-z]{2,6}$', 'Incorrect address format E-mail');
                    $form->addElement($htmlElement);
                    break;
                case "captcha":
                    $htmlElement = new captchaHTMLelement($element['name'], $element, $element['req']);
                    $form->addElement($htmlElement);
                    break;
                
            }
        }
        
        return $form->show();
    }

}

Как видно, не требуется создавать отдельные классы для каждого элемента с особенными правилами для вводимых значений, для этого достаточно наделить правилами более простой элемент.

Валидация на стороне сервера

Здесь проще, не нужно заниматься дизайном, просто проверить пришедшие на сервер значения. И для этих целей я создал отдельный класс, который подключается в месте, где необходимо проверить данные. У меня это выглядит так:

$validForm = new validatorClass($this->params[1]);
$validForm->loadParams($this->post);
if ($validForm->isError == false)
     {
       // Ошибок нет, можно использовать данные
     }
   else
     {
        // Значения не прошли валидацию, описание ошибки хранится в $validForm->errorText
     }   

Далее рассмотрим сам класс validatorClass:

class validatorClass
 {
   protected $src, $name;
   public $errorText, $isError;
   
    public function __construct($name)
   {
    $this->src = '../application/userdata/validator/'.$name.'Validator.xml';
    if(!is_file($this->src)) new CustomException('Not found: '.$this->src);
    $this->name = $name;
    $this->isError = false;
   }
   
   public function loadParams($params)
   {
    $xml = simplexml_load_file($this->src);
   foreach($xml->field as $field)
     {
       $value = $params[''.$field['name'].''];
       
       if (($field['req']=="true")and($field['type'] != "submit"))
        {
          if ($value == ""){$this->isError = true; $this->errorText = 'The < '.$field.' > field can not be empty';break;}
        }
       if (($field['min'])and($value))
        {
          $min = (int) $field['min'];
          if (mb_strlen($value, CHARSET) < $min){$this->isError = true; $this->errorText = 'Поле < '.$field.' > должно содержать минимум '.$min.' символов';break;}
        }
       if ($field['max'])
        {
          $max = (int) $field['max'];
          if (mb_strlen($value, CHARSET) > $max){$this->isError = true; $this->errorText = 'Поле < '.$field.' > превышает максимальных '.$max.' символа';break;}
        } 
       if ($field['type']=="word")
        {
         $pattern = '/^([-_0-9a-zA-Zа-яА-Я]+)$/iu';
         $n = preg_match($pattern, $value);
         if ($n == 0){$this->isError = true; $this->errorText = 'The < '.$field.' > field can only contain';break;}
        }
       if ($field['type']=="email")
        {
         $pattern = '/^([a-z0-9+_-]+)(.[a-z0-9+_-]+)*@([a-z0-9-]+.)+[a-z]{2,6}$/';
         $n = preg_match($pattern, $value);
         if ($n == 0){$this->isError = true; $this->errorText = 'Incorrect E-mail format in '.$field.'field';break;}
        } 
     } 
   } 
 }

Класс не совсем аккуратно оформлен, давно им не занимался, но чтобы был понятен смысл, я привёл его. Вы скорее всего оформите его более красиво.

Заключение

Предвижу, что многие начнут писать что-то вроде "Зачем изобретать велосипед?" и тому подобное, поэтому сразу отвечу:
— А вы учились писать на PHP, сразу используя готовые библиотеки?

Учиться необходимо — вникая во всё и экспериментируя с кодом. Я просто захотел написать свою реализацию, не потому что я считаю себя самым умным, а потому, что я хочу писать, экспериментировать и напрягать мозги — я получаю от этого удовольствие. Надеюсь, среди вас найдутся такие же.

А что касается профессиональных и высоко ответственных сайтов, однозначно необходимо использовать проверенные временем решения.

Спасибо за внимание!

Автор: nskforward

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js