AMatch, часть 2. Коды ошибок, собственные ошибки, новый формат callback

в 14:32, , рубрики: amatch, pattern matching, php, validation, ооп, Программирование, метки: , , , ,

В этой статье я расскажу о некоторых новшествах, появившихся в проекте AMatch с момента написания первой статьи.

Напомню, что AMatch — класс, с помощью которого валидация входных параметров из большого набора if-ов превращается в удобную, лаконичную запись. К примеру:

Example: simple

$match = AMatch::runMatch($params)
	->doc_id(0, '<') // Левое значение меньше
	->subject_id(0, '!=') // Не равен нулю
;
$result = $match->stopMatch();
if (!$result) {
	die(var_export($match->matchComments(), true)); // для наглядности умрём
}

Входные данные

В дальнейших примерах будем проверять массив в «хорошем» и «плохом» варианте:

Example: input data

$params = array(
	'subject_id' => '64',
	'parent_id' => -32,
	'delimeter' => '-4.645E+32',
	'title' => 'New document',
	'links' => array(13, '-16', 24),
	'email' => 'someuser@mail.dom',
);
$params_bad = array(
	'subject_id' => '64.43',
	'parent_id' => array(),
	'delimeter' => '-4.x6E.32',
	'title' => new stdClass(),
	'links' => array(0, array(0, array(0)), 0),
	'email' => 'someuser!@mail.dom',
);

Для проверки результатов тестов напишем функцию:

Example: result function

function result(AMatch $match)
{
	echo PHP_EOL; echo $match->stopMatch() ? 'Dance!' : 'Cry!' ;
	echo PHP_EOL; var_export($match->matchResults());
	echo PHP_EOL; var_export($match->matchComments());
	echo PHP_EOL; var_export($match->matchCommentsConditions());
}

Работа над ошибками

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

Получение кодов ошибок

Составим простое условие валидации и отправим туда последовательно хороший и плохой массивы.

Example: bad and good policeman

$match = AMatch::runMatch($params, AMatch::FLAG_SHOW_GOOD_COMMENTS)->delimeter('', 'float'); // Существует с указанным типом
result($match);
$match = AMatch::runMatch($params_bad, AMatch::FLAG_SHOW_GOOD_COMMENTS)->delimeter('', 'float');
result($match);

В результате получим следующий ответ

// Хороший массив
Dance!
array (
  'delimeter' => 103,
)
array (
  'delimeter' => 'OK. Expected parameter type is valid',
)
array (
  'delimeter' =>
  array (
    0 => '',
    1 => 'float',
  ),
)
// Плохой массив
Cry!
array (
  'delimeter' => 3,
)
array (
  'delimeter' => 'Expected parameter type is not valid',
)
array (
  'delimeter' =>
  array (
    0 => 'float',
    1 => 'float',
  ),
)

Как видно из примера:
— matchResults() вернёт коды ошибок,
— matchComments() — комментарии,
— matchCommentsConditions() — условие валидации и дополнительная информация.

Дополнительная информация про float в AMatch
Обратите внимание, что float проверяется не через is_float
// Валидные значения float:
1, -1, 1.0, -1.0, '1', '-1', '1.0', '-1.0', '2.1', '0', 0, ' 0 ', ' 0.1 ', ' -0.0 ', -0.0, 3., '-3.', '.27', .27, '-0', '+4', '1e2', '+1353.0316547', '13213.032468e-13465', '-8E+3', '-1354.98879e+37436'

// Невалидные значения:
false, true, '', '-', '.a', '-1.a', '.a', '.', '-.', '1+', '1.3+', 'a1', 'e.e', '-e-4', 'e2', '8e', '3,25', '1.1.1'

Маппинг ошибок

Попробуем написать функцию, которая сделает вывод ошибок «наружу» в соответствие с неким принятым стандартом внутри имеющегося кода.

Example: mapping

function mapping(AMatch $match)
{
	// Карта ошибок
	$errors_mapping = array(
			AMatchStatus::KEY_TYPE_NOT_VALID => 'invalid_type',
			AMatchStatus::KEY_CONDITION_NOT_VALID => 'invalid_data',
			AMatchStatus::KEY_NOT_EXISTS => 'required',
		);
	$results = $match->matchResults(); // Результат в кодах
	$comments = $match->matchComments(); // Комментарий к результату
	$comments_conditions = $match->matchCommentsConditions(); // Расшифровка результата
	$output = array();
	foreach ($results as $param => $result) {
		$error = array_key_exists($result, $errors_mapping)
			? $errors_mapping[$result] : 'other_errors'; // Ошибка, не имеющая аналогов в карте
		$comment = $param . ': ' . $comments[$param];
		if (isset($comments_conditions[$param]) && !empty($comments_conditions[$param][0])) {
			$comment .= ' (' . $comments_conditions[$param][0] . ')'; // Дополнительная информация
		}
		$output[$error][] = $comment;
	}
	var_export($output);
}
$match = AMatch::runMatch($params_bad, AMatch::FLAG_SHOW_GOOD_COMMENTS | AMatch::FLAG_DONT_STOP_MATCHING)
	->title('', 'string') // Существует с типом string
	->parent_id('', 'int') // Существует с типом string
	->ineedkey() // Ключ должен существовать
	->subject_id(1, '>') // "1" больше имеющегося значения
	->delimeter('', 'blabla') // Ошибка в условии
	;
mapping($match);

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

Результат примера «mapping»

array (
  'invalid_type' =>
  array (
    0 => 'title: Expected parameter type is not valid (string)',
    1 => 'parent_id: Expected parameter type is not valid (int)',
  ),
  'required' =>
  array (
    0 => 'ineedkey: Expected parameter does not exist in the array of parameters',
  ),
  'invalid_data' =>
  array (
    0 => 'subject_id: Condition is not valid (1)',
  ),
  'other_errors' =>
  array (
    0 => 'delimeter: Condition is unknown',
  ),
)

Собственная расшифровка ошибок для любого условия

Для того, чтобы на любое условие получить собственную ошибку в комментариях (в случае непрохождения данного условия), достаточно передать текст третим параметром. Добавим в описанном выше примере («mapping») свой текст к ошибке:

Example: unique errors

...
->title('', 'string', 'Incorrect document title. Please, read FAQ.')
...
И выполним его заново

array (
  'other_errors' =>
  array (
    0 => 'title: Incorrect document title. Please, read FAQ. (string)',
    1 => 'delimeter: Condition is unknown',
  ),

Но заменять каждое условие — не всегда нужно. Иногда нужно заменить все комментарии.

Подмена класса с ошибками (в т.ч. i18n)

Снова вернёмся к примеру «mapping». Для полноценной замены всех необходимых ошибок, напишем класс-наследник от AMatchStatus. Внутри необходимо перегрузить метод _fillComments(), не забывая вызвать родительский. Нужно создать объект данного класса и передать его в AMatch::runMatch(); третим параметром.

Example: russian

class AMatchRussian extends AMatchStatus
{
	protected function _fillComments()
	{
		parent::_fillComments(); // Если не вызвать родительский метод, то отсутствующие строки будут отданы в виде кодов
		$this->_result_comments[self::KEY_NOT_EXISTS] = 'Искал, вот честно. Не нашел';
		$this->_result_comments[self::KEY_CONDITION_NOT_VALID] = 'Параметр не соответствует требованиям, попробуйте поиграть шрифтами';
		$this->_result_comments[self::CONDITION_IS_UNKNOWN] = 'Нипаняятна';
	}
}

$match = AMatch::runMatch($params_bad, AMatch::FLAG_SHOW_GOOD_COMMENTS | AMatch::FLAG_DONT_STOP_MATCHING, new AMatchRussian())
// ... дальше те жепроверки, что и в примере mapping
;
mapping($match);

Ответ будет содержать переведённые конструкции наравне с не имеющими перевода.

Результат примера «russian»

array (
  'other_errors' =>
  array (
    0 => 'title: Incorrect document title. Please, read FAQ. (string)',
    1 => 'delimeter: Нипаняятна',
  ),
  'invalid_type' =>
  array (
    0 => 'parent_id: Expected parameter type is not valid (int)',
  ),
  'required' =>
  array (
    0 => 'ineedkey: Искал, вот честно. Не нашел',
  ),
  'invalid_data' =>
  array (
    0 => 'subject_id: Попробуйте поиграть шрифтами (1)',
  ),
)

Новые возможности callback

Помимо отслеживания ошибок, несколько усовершенствована работа с пользовательскими функциями. Напомню, что ранее их можно было вызвать следующим образом:

$match->data(array($this, 'callbackMethod'), 'callback');

На текущий момент поддержка callback расширена, и теперь можно вызывать по следующей схеме:

Example: callback

// param([mixed $callback_argument], [callable|callable $callback])
// или
// param([callable|string $callback], 'callback')

// Примеры:
->param($callback_property, 'MyClass::myFunc')
->param($callback_property, 'MyClass->myFunc')
->param($callback_property, array($my_obj, 'myFunc'))
->param($callback_property, array('MyClass', 'myFunc'))

Встроенные callback-плагины

Вместе с библиотекой AMatch лежат примеры готовых классов с методами, которые можно использовать в качестве пользовательского вызова. Это два класса:

  1. class AMatchArray
  2. class AMatchString

Рассмотрим примеры использования:

Example: plugins

function matchCallbacks($params)
{
	$match = AMatch::runMatch($params, AMatch::FLAG_DONT_STOP_MATCHING)
	->parent_id('/^-?d+$/', 'AMatchString::pregMatch') // проверка значения регулярным выражением
	->title(12, 'AMatchString::length') // длина строки равна
	->email('([w-]+@([w-]+.)+[w-]+)', 'AMatchString::isEmail') // проверка email собственной регуляркой (игнорируя встроенный алгоритм)
	->links(AMatchArray::FLAG_EMPTY_SOME_ELEMENT, 'AMatchArray::isNotEmpty') // проверка на пустоту по алгоритму: хотя бы один элемен массива или его вложенных массивов должен быть не-пустым
	;
	result($match);
}
matchCallbacks($params);
matchCallbacks($params_bad);
Результат примера «plugins»

// Хороший массив
Dance!
array (
)
array (
)
array (
)
// Плохой массив
Cry!
array (
  'parent_id' => 'str3',
  'title' => 'str5',
  'email' => 'str4',
  'links' => 'arr8',
)
array (
  'parent_id' => 'The string does not match the regular expression',
  'title' => 'String required',
  'email' => 'Incorrect email',
  'links' => 'At least one element of the array must be non-empty',
)
array (
  'parent_id' =>
  array (
    0 => '/^-?\d+$/',
    1 => 'AMatchString::pregMatch',
  ),
  'title' =>
  array (
    0 => 12,
    1 => 'AMatchString::length',
  ),
  'email' =>
  array (
    0 => 'someuser!@mail.dom',
    1 => 'AMatchString::isEmail',
  ),
  'links' =>
  array (
    0 => NULL,
    1 => 'AMatchArray::isNotEmpty',
  ),
)

Послебуквие

Общая схема вызова AMatch на данный момент следующая:

$match = 
AMatch::runMatch(array $associative_array, bitmask $flags, AMatchStatus $obj)
->имя_ключа([ожидаемое_или_сравниваемое_значение], [условие])
->…;

$match->stopMatch(); // получить общий результат
$match->matchResults(); // Получить коды статусов (ошибки или успешные статусы, если предусмотрены)
$match->matchComments(); // Получить комментарии
$match->matchCommentsConditions(); // Получить дополнительную информацию

Вы можете найти больше примеров (смотрите unittests) и скачать исходники AMatch на гитхабе, где вас ждёт готовый файл с примерами для этой статьи (examples2.php).

Автор: KIVagant


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


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