Нетте-схема
Практическая библиотека для проверки и нормализации структур данных по заданной схеме с умным и простым для понимания API.
Документацию можно найти на сайте.
Установка:
composer require nette/schema
Для этого требуется PHP версии 8.1 и поддерживается PHP до 8.4.
Вам нравится Nette Schema? Вы с нетерпением ждете новых функций?
Спасибо!
В переменной $schema
у нас есть схема валидации (что именно это означает и как ее создать мы скажем позже), а в переменной $data
у нас есть структура данных, которую мы хотим проверить и нормализовать. Это могут быть, например, данные, отправленные пользователем через API, файл конфигурации и т. д.
Задача обрабатывается классом NetteSchemaProcessor, который обрабатывает входные данные и либо возвращает нормализованные данные, либо в случае ошибки выдает исключение NetteSchemaValidationException.
$ processor = new Nette Schema Processor ;
try {
$ normalized = $ processor -> process ( $ schema , $ data );
} catch ( Nette Schema ValidationException $ e ) {
echo ' Data is invalid: ' . $ e -> getMessage ();
}
Метод $e->getMessages()
возвращает массив всех строк сообщений, а метод $e->getMessageObjects()
возвращает все сообщения как объекты NetteSchemaMessage.
А теперь давайте создадим схему. Для его определения используется класс NetteSchemaExpect, мы фактически определяем ожидания того, как должны выглядеть данные. Допустим, входные данные должны представлять собой структуру (например, массив) processRefund
содержащую элементыprocessRefund типа bool refundAmount
типа int.
use Nette Schema Expect ;
$ schema = Expect:: structure ([
' processRefund ' => Expect:: bool (),
' refundAmount ' => Expect:: int (),
]);
Мы считаем, что определение схемы выглядит понятным, даже если вы видите его впервые.
Давайте отправим следующие данные для проверки:
$ data = [
' processRefund ' => true ,
' refundAmount ' => 17 ,
];
$ normalized = $ processor -> process ( $ schema , $ data ); // OK, it passes
Результатом, то есть значением $normalized
, является объект stdClass
. Если мы хотим, чтобы выходные данные были массивом, мы добавляем приведение к схеме Expect::structure([...])->castTo('array')
.
Все элементы структуры являются необязательными и имеют значение по умолчанию null
. Пример:
$ data = [
' refundAmount ' => 17 ,
];
$ normalized = $ processor -> process ( $ schema , $ data ); // OK, it passes
// $normalized = {'processRefund' => null, 'refundAmount' => 17}
Тот факт, что значение по умолчанию равно null
, не означает, что оно будет принято во входных данных 'processRefund' => null
. Нет, ввод должен быть логическим, т.е. только true
или false
. Нам пришлось бы явно разрешить null
через Expect::bool()->nullable()
.
Элемент можно сделать обязательным, используя Expect::bool()->required()
. Мы меняем значение по умолчанию на false
используя Expect::bool()->default(false)
или вскоре используя Expect::bool(false)
.
А что, если мы хотим принимать помимо логических значений 1
и 0
? Затем мы перечисляем разрешенные значения, которые мы также нормализуем до логических значений:
$ schema = Expect:: structure ([
' processRefund ' => Expect:: anyOf ( true , false , 1 , 0 )-> castTo ( ' bool ' ),
' refundAmount ' => Expect:: int (),
]);
$ normalized = $ processor -> process ( $ schema , $ data );
is_bool ( $ normalized -> processRefund ); // true
Теперь вы знаете основы определения схемы и поведения отдельных элементов структуры. Теперь мы покажем, какие все остальные элементы можно использовать при определении схемы.
В схеме можно перечислить все стандартные типы данных PHP:
Expect:: string ( $ default = null )
Expect:: int ( $ default = null )
Expect:: float ( $ default = null )
Expect:: bool ( $ default = null )
Expect::null()
Expect:: array ( $ default = [])
А затем все типы, поддерживаемые валидаторами, через Expect::type('scalar')
или сокращенно Expect::scalar()
. Также принимаются имена классов или интерфейсов, например Expect::type('AddressEntity')
.
Вы также можете использовать обозначение объединения:
Expect:: type ( ' bool|string|array ' )
Значение по умолчанию всегда null
за исключением array
и list
, где это пустой массив. (Список — это массив, индексированный в порядке возрастания числовых ключей от нуля, то есть неассоциативный массив).
Массив — слишком общая структура, полезнее указать, какие именно элементы он может содержать. Например, массив, элементами которого могут быть только строки:
$ schema = Expect:: arrayOf ( ' string ' );
$ processor -> process ( $ schema , [ ' hello ' , ' world ' ]); // OK
$ processor -> process ( $ schema , [ ' a ' => ' hello ' , ' b ' => ' world ' ]); // OK
$ processor -> process ( $ schema , [ ' key ' => 123 ]); // ERROR: 123 is not a string
Второй параметр можно использовать для указания ключей (начиная с версии 1.2):
$ schema = Expect:: arrayOf ( ' string ' , ' int ' );
$ processor -> process ( $ schema , [ ' hello ' , ' world ' ]); // OK
$ processor -> process ( $ schema , [ ' a ' => ' hello ' ]); // ERROR: 'a' is not int
Список представляет собой индексированный массив:
$ schema = Expect:: listOf ( ' string ' );
$ processor -> process ( $ schema , [ ' a ' , ' b ' ]); // OK
$ processor -> process ( $ schema , [ ' a ' , 123 ]); // ERROR: 123 is not a string
$ processor -> process ( $ schema , [ ' key ' => ' a ' ]); // ERROR: is not a list
$ processor -> process ( $ schema , [ 1 => ' a ' , 0 => ' b ' ]); // ERROR: is not a list
Параметр также может быть схемой, поэтому мы можем написать:
Expect:: arrayOf (Expect:: bool ())
Значение по умолчанию — пустой массив. Если вы укажете значение по умолчанию и вызовете mergeDefaults()
, оно будет объединено с переданными данными.
anyOf()
— это набор значений или схем, которыми может быть значение. Вот как написать массив элементов, которые могут иметь значение 'a'
, true
или null
:
$ schema = Expect:: listOf (
Expect:: anyOf ( ' a ' , true , null ),
);
$ processor -> process ( $ schema , [ ' a ' , true , null , ' a ' ]); // OK
$ processor -> process ( $ schema , [ ' a ' , false ]); // ERROR: false does not belong there
Элементами перечисления также могут быть схемы:
$ schema = Expect:: listOf (
Expect:: anyOf (Expect:: string (), true , null ),
);
$ processor -> process ( $ schema , [ ' foo ' , true , null , ' bar ' ]); // OK
$ processor -> process ( $ schema , [ 123 ]); // ERROR
Метод anyOf()
принимает варианты как отдельные параметры, а не как массив. Чтобы передать ему массив значений, используйте оператор распаковки anyOf(...$variants)
.
Значение по умолчанию — null
. Используйте метод firstIsDefault()
чтобы сделать первый элемент элементом по умолчанию:
// default is 'hello'
Expect:: anyOf (Expect:: string ( ' hello ' ), true , null )-> firstIsDefault ();
Структуры — это объекты с определенными ключами. Каждая из этих пар ключ => значение называется «свойством»:
Структуры принимают массивы и объекты и возвращают объекты stdClass
(если вы не измените его с помощью castTo('array')
и т. д.).
По умолчанию все свойства являются необязательными и имеют значение по умолчанию null
. Вы можете определить обязательные свойства, используя required()
:
$ schema = Expect:: structure ([
' required ' => Expect:: string ()-> required (),
' optional ' => Expect:: string (), // the default value is null
]);
$ processor -> process ( $ schema , [ ' optional ' => '' ]);
// ERROR: option 'required' is missing
$ processor -> process ( $ schema , [ ' required ' => ' foo ' ]);
// OK, returns {'required' => 'foo', 'optional' => null}
Если вы не хотите выводить свойства только со значением по умолчанию, используйте skipDefaults()
:
$ schema = Expect:: structure ([
' required ' => Expect:: string ()-> required (),
' optional ' => Expect:: string (),
])-> skipDefaults ();
$ processor -> process ( $ schema , [ ' required ' => ' foo ' ]);
// OK, returns {'required' => 'foo'}
Хотя значением по умолчанию для optional
свойства является null
, оно не допускается во входных данных (значение должно быть строкой). Свойства, принимающие значение null
определяются с помощью nullable()
:
$ schema = Expect:: structure ([
' optional ' => Expect:: string (),
' nullable ' => Expect:: string ()-> nullable (),
]);
$ processor -> process ( $ schema , [ ' optional ' => null ]);
// ERROR: 'optional' expects to be string, null given.
$ processor -> process ( $ schema , [ ' nullable ' => null ]);
// OK, returns {'optional' => null, 'nullable' => null}
По умолчанию во входных данных не может быть лишних элементов:
$ schema = Expect:: structure ([
' key ' => Expect:: string (),
]);
$ processor -> process ( $ schema , [ ' additional ' => 1 ]);
// ERROR: Unexpected item 'additional'
Что мы можем изменить с otherItems()
. В качестве параметра укажем схему для каждого лишнего элемента:
$ schema = Expect:: structure ([
' key ' => Expect:: string (),
])-> otherItems (Expect:: int ());
$ processor -> process ( $ schema , [ ' additional ' => 1 ]); // OK
$ processor -> process ( $ schema , [ ' additional ' => true ]); // ERROR
Вы можете объявить свойство устаревшим, используя метод deprecated([string $message])
. Уведомления об устаревании возвращаются функцией $processor->getWarnings()
:
$ schema = Expect:: structure ([
' old ' => Expect:: int ()-> deprecated ( ' The item %path% is deprecated ' ),
]);
$ processor -> process ( $ schema , [ ' old ' => 1 ]); // OK
$ processor -> getWarnings (); // ["The item 'old' is deprecated"]
Используйте min()
и max()
чтобы ограничить количество элементов в массивах:
// array, at least 10 items, maximum 20 items
Expect:: array ()-> min ( 10 )-> max ( 20 );
Для строк ограничьте их длину:
// string, at least 10 characters long, maximum 20 characters
Expect:: string ()-> min ( 10 )-> max ( 20 );
Для чисел ограничьте их значение:
// integer, between 10 and 20 inclusive
Expect:: int ()-> min ( 10 )-> max ( 20 );
Конечно, можно упомянуть только min()
или только max()
:
// string, maximum 20 characters
Expect:: string ()-> max ( 20 );
Используя pattern()
, вы можете указать регулярное выражение, которому должна соответствовать вся входная строка (т.е. как если бы она была заключена в символы ^
a $
):
// just 9 digits
Expect:: string ()-> pattern ( ' d{9} ' );
Вы можете добавить любые другие ограничения, используя assert(callable $fn)
.
$ countIsEven = fn ( $ v ) => count ( $ v ) % 2 === 0 ;
$ schema = Expect:: arrayOf ( ' string ' )
-> assert ( $ countIsEven ); // the count must be even
$ processor -> process ( $ schema , [ ' a ' , ' b ' ]); // OK
$ processor -> process ( $ schema , [ ' a ' , ' b ' , ' c ' ]); // ERROR: 3 is not even
Или
Expect:: string ()-> assert ( ' is_file ' ); // the file must exist
Для каждого утверждения можно добавить собственное описание. Это будет часть сообщения об ошибке.
$ schema = Expect:: arrayOf ( ' string ' )
-> assert ( $ countIsEven , ' Even items in array ' );
$ processor -> process ( $ schema , [ ' a ' , ' b ' , ' c ' ]);
// Failed assertion "Even items in array" for item with value array.
Метод можно вызывать повторно, чтобы добавить несколько ограничений. Его можно смешивать с вызовами transform()
и castTo()
.
Успешно проверенные данные можно изменить с помощью специальной функции:
// conversion to uppercase:
Expect:: string ()-> transform ( fn ( string $ s ) => strtoupper ( $ s ));
Метод можно вызывать повторно для добавления нескольких преобразований. Его можно смешивать с вызовами assert()
и castTo()
. Операции будут выполняться в том порядке, в котором они объявлены:
Expect:: type ( ' string|int ' )
-> castTo ( ' string ' )
-> assert ( ' ctype_lower ' , ' All characters must be lowercased ' )
-> transform ( fn ( string $ s ) => strtoupper ( $ s )); // conversion to uppercase
Метод transform()
может одновременно преобразовывать и проверять значение. Зачастую это проще и менее избыточно, чем объединение в цепочку transform()
и assert()
. Для этого функция получает объект NetteSchemaContext с методом addError()
, который можно использовать для добавления информации о проблемах проверки:
Expect:: string ()
-> transform ( function ( string $ s , Nette Schema Context $ context ) {
if (! ctype_lower ( $ s )) {
$ context -> addError ( ' All characters must be lowercased ' , ' my.case.error ' );
return null ;
}
return strtoupper ( $ s );
});
Успешно проверенные данные могут быть преобразованы:
Expect:: scalar ()-> castTo ( ' string ' );
Помимо собственных типов PHP, вы также можете выполнять приведение к классам. Он различает, является ли это простым классом без конструктора или классом с конструктором. Если у класса нет конструктора, создается его экземпляр и в его свойства записываются все элементы структуры:
class Info
{
public bool $ processRefund ;
public int $ refundAmount ;
}
Expect:: structure ([
' processRefund ' => Expect:: bool (),
' refundAmount ' => Expect:: int (),
])-> castTo (Info::class);
// creates '$obj = new Info' and writes to $obj->processRefund and $obj->refundAmount
Если у класса есть конструктор, элементы структуры передаются конструктору как именованные параметры:
class Info
{
public function __construct (
public bool $ processRefund ,
public int $ refundAmount ,
) {
}
}
// creates $obj = new Info(processRefund: ..., refundAmount: ...)
Приведение в сочетании со скалярным параметром создает объект и передает значение в качестве единственного параметра конструктору:
Expect:: string ()-> castTo (DateTime::class);
// creates new DateTime(...)
Перед самой проверкой данные можно нормализовать с помощью метода before()
. В качестве примера давайте возьмем элемент, который должен быть массивом строк (например, ['a', 'b', 'c']
), но получает входные данные в виде строки abc
:
$ explode = fn ( $ v ) => explode ( ' ' , $ v );
$ schema = Expect:: arrayOf ( ' string ' )
-> before ( $ explode );
$ normalized = $ processor -> process ( $ schema , ' a b c ' );
// OK, returns ['a', 'b', 'c']
Вы можете создать структурную схему из класса. Пример:
class Config
{
public string $ name ;
public ? string $ password ;
public bool $ admin = false ;
}
$ schema = Expect:: from ( new Config );
$ data = [
' name ' => ' jeff ' ,
];
$ normalized = $ processor -> process ( $ schema , $ data );
// $normalized instanceof Config
// $normalized = {'name' => 'jeff', 'password' => null, 'admin' => false}
Также поддерживаются анонимные классы:
$ schema = Expect:: from ( new class {
public string $ name ;
public ? string $ password ;
public bool $ admin = false ;
});
Поскольку информации, полученной из определения класса, может быть недостаточно, вы можете добавить собственную схему для элементов со вторым параметром:
$ schema = Expect:: from ( new Config , [
' name ' => Expect:: string ()-> pattern ( ' w:.* ' ),
]);