Pull to refresh

Strain It! — Валидация и приведение данных на PHP

Reading time 6 min
Views 3.6K

Введение


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

Но после того как я перешел на PHP 5.3 меня потянуло написать новый валидатор на основе анонимных функций. А заодно еще раз потренироваться в их использовании. Мною ставились следующие требования к будущему классу:
  • Декларативность (информация о валидации должна задаваться объектами/массивами)
  • Гибкость (высокая эффективность кода, написанного с помощью этого класса)
  • Скорость (ничего лишнего)

Уже после начала работ, мне пришла в голову мысль о том, что анонимные функции в этом классе можно использовать не только для валидации, но и для приведения данных к нужному виду (например к определенному типу). Добавив к списку требований еще одно («Универсальность») я смело переименовал класс в Strain.

Что же в итоге получилось… читаем ниже. Здесь я не буду претендовать на оригинальность решения и прочую ерунду. Если вам понравится такой подход, используйте его в своих проектах, а если нет — то нет. Для отчаянных любителей извращений исходники я опубликую в конце статьи.

Class Strain


Как я уже написал выше, функционал у класса двойной. Давайте по порядку рассмотрим оба вида использующихся в нём анонимных функций и их применение. Разумеется ничто не мешает вам при желании объединить их функционал в одной функции.

Надеюсь я не сильно вас напугаю наспех придуманными названиями и определениями.

Функция-приведения (ФП)

ФП для преобразования данных (в нашем случае некой переменной). По сути может содержать любую логику по превращению одних данных в другие. Например создадим функцию, которая будет всё переводить в INTEGER.
Strain::add('integer', function(&$value, $options = null) {<br/>
    $value = (int) $value;<br/>
});

Это лишь простенький пример. При реальном использовании я бы сюда добавил еще ограничение по количеству бит в числе.

Теперь давайте посмотрим на работу этой функции:
$var = '56'; // обязательно переменная! потому что функция заберет её ссылку.<br/>
Strain::it($var, 'integer');<br/>
var_dump($var);<br/>
 <br/>
// Выводит: int(56)

Функция-валидации (ФВ)

ФВ проверяет данные на соответствие определенным условием и в случае ошибки выкидывает отчет. Если функция ничего не вернет (т.е. вернет NULL), значит данные верны. Пример:
Strain::add('must_be_integer', function(&$value, $options = null) {<br/>
    if(!is_int($value)) return true;<br/>
});

Комментарии излишни. Давайте посмотрим на работу функции:

$var = '56';<br/>
var_dump(Strain::it($var, 'must_be_integer'));<br/>
 <br/>
// Выводит: TRUE

TRUE указывает на наличие ошибки. Более подробно вернувшееся значение можно посмотреть в Strain::$result (в данном примере оно совпадает с TRUE)

Сложные пример

Теперь после того, как мы разобрались с основами, настало время перейти к более сложному применению Strain — валидации объектов.

Для начала создадим сам объект с данными. И пусть это будет, к примеру, данные о новом пользователе, которого мы хотим добавить.
$user = (object) array(<br/>
    'email' => 'user@site.com',<br/>
    'name' => 'User',<br/>
    'address' => (object) array(<br/>
        'city' => 'Default City',<br/>
        'street' => 'Street'<br/>
    )<br/>
);

И так… мы имеем некий объект, который должен содержать корректные данные и иметь нужную нам структуру, и мы должны в этом удостовериться прежде чем кидать его в базу, причем мы должны точно узнать о всех ошибках валидации. К счастью Strain может полностью решить эту задачу.
Создадим объект, с помощью которого будет происходить валидация. В дальнейшем такие объекты я буду называть «схемой фильтрации данных» (СФ).
$valid = (object) array(<br/>
    'email' => array('email', 'UserExists'),<br/>
    'name' => array('string', 'regexp' => '/^[A-Za-z0-9 _-]{3,20}$/'),<br/>
    'address' => (object) array(<br/>
        'city' => 'string',<br/>
        'street' => 'string'<br/>
    )<br/>
);

Давайте подробно рассмотрим СФ. Сразу заметно, что объект полностью повторяет структуру проверяемых данных, только вот значения свойств какие-то «странные». Уверен, многие из вас уже догадались как именно составлялся этот объект.

Каждое значение объекта содержит либо название функции, либо другую ФС. Например в виде списка названий функций — массива смешанного типа, с возможностью передачи параметров в функцию. Помните ту непонятную переменную $options в задании функций? это оно!

Теперь приступим к проверке. Тут уже знакомый нам вызов.
Strain::it($user, $valid); // Вернет FALSE так как всё корректно

Не забываем посмотреть, что осталось в Strain::$result
object(stdClass)#7 (3) {<br/>
  ["email"]=><br/>
  NULL<br/>
  ["name"]=><br/>
  NULL<br/>
  ["address"]=><br/>
  object(stdClass)#9 (2) {<br/>
    ["city"]=><br/>
    NULL<br/>
    ["street"]=><br/>
    NULL<br/>
  }<br/>
}

Как мы видим объект прошел фильтрацию не оставив ошибок.

Дополнительная гибкость метода заключается в том, что с помощью метода Strain::add() можно добавлять не только сами анонимные функции, но и СФ, которые вы видели выше. Также возможны рекурсии, когда одна из функций запускает собственную проверку. (см. ниже)

Парочка замечаний

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

Если данные проверяются списком функций, то в $result попадёт результат от первой функции, которая чтото вернула. НО если функция вернет FALSE, то в результат уйдет NULL, а выполнение цепочки остановится. Это сделано для того, чтобы иметь простой способ остановить проверку не возвращая ошибку, например в случае, если значение может быть равным NULL, а может быть задано, и нужно выполнить проверку. Пример:
$valid = array('null', 'string', 'length' => array(2,10));

В функции 'null' нужно написать, чтобы она возвращала FALSE, если ей пришло значение NULL, тогда дальнейшая проверка не будет проводиться и никакой ошибки не появится.

Реализация проверки массива с однотипными данными.
$valid = array('array_of' => array('string', 'length' => array(2,10)));

Пример того, как в функцию легко передаются СФ, которые она может использовать по своему усмотрению. В данном случае, ими проверяется каждый элемент массива.

Еще один интересный пример. Реализации условия ИЛИ.
$valid = array('mixed' => array('null', array('string', 'length' => array(2, 10))));

Делает то же самое, что пример с NULL выше по тексту, только без обязательного возвращения FALSE этой функцией. Аналогично можно придумать функции реализующие различные XOR, NOR и BRRR!

В заключении, хочу рассказать про третий параметр метода Strain::it(). Он определяет поведение, применяемое к данным. Значения параметра:

0: Не меняет структуру данных;
1: Добавляет в структуру данных свойства указанные в структуре фильтров;
2: Удаляет из структуры данных свойства не указанные в структуре фильтров;
3: 1 и 2 вместе. (По умолчанию. Только с этим значением гарантируется, что объект после фильтрации будет иметь структуру аналогичную СФ, даже если вместо объекта сунуть какой-нибудь NULL)

Т.е. класс умеет удалять лишние свойства объектов, добавлять им новые и заменять то, что не соотвествует СФ. Созданные свойства по умолчанию имеют значение NULL и отдаются на фильтрацию.

Заключение


К сожалению, в исходники я не включил анонимные функции, кроме 'array_of' и 'mixed'. В любом случае, писать их довольно легко, и каждый все равно захочет их сделать по-своему.

Для удобства использования и наглядности кода, рекомендую разделять написание названий ФП и ФВ. Например всем ФП давать имена содержащие в начале (или в конце) символ `!`. Или же придумать параметр, который будет явно указывать на логику.

Важно! В данный момент класс еще только тестируется и использовать его в product вы можете только на свой страх и риск.

Как и обещал, ссылка на исходники:
Исходники Strain на GitHub

P.S. А кому-нибудь нужно нечто похожее, только на JS?

Апдейт: Если кого-то очень путает, что метод it() возращает TRUE, когда есть ошибка и FALSE, когда её нету, а не наоборот, то они могут поправить его код. На работе класса это никак не отразится.
Апдейт 2: В класс добавлен метод check(), который аналогичен методу it(), но выдает TRUE, если нет ошибок и FALSE если есть.
Апдейт 3: Добавлена возможность указывать функции непосредственно в схеме, а также возможность вставлять в массивы-схемы объекты-схемы, что пожалуй тянет на еще одну статью, потому что в двух словах это не объяснишь. Один из примеров я выложил в комментариях.
Tags:
Hubs:
+5
Comments 25
Comments Comments 25

Articles