Schema: επικύρωση δεδομένων
Μια πρακτική βιβλιοθήκη για την επικύρωση και την κανονικοποίηση δομών δεδομένων έναντι ενός δεδομένου schema με ένα έξυπνο, κατανοητό API.
Εγκατάσταση:
composer require nette/schema
Βασική χρήση
Στη μεταβλητή $schema
έχουμε το schema επικύρωσης (τι ακριβώς
σημαίνει αυτό και πώς να δημιουργήσετε ένα τέτοιο schema θα πούμε αμέσως)
και στη μεταβλητή $data
τη δομή δεδομένων που θέλουμε να
επικυρώσουμε και να κανονικοποιήσουμε. Μπορεί να πρόκειται, για
παράδειγμα, για δεδομένα που στάλθηκαν από τον χρήστη μέσω μιας
διεπαφής API, ενός αρχείου διαμόρφωσης, κ.λπ.
Την εργασία αναλαμβάνει η κλάση Nette\Schema\Processor, η οποία επεξεργάζεται την είσοδο και είτε επιστρέφει τα κανονικοποιημένα δεδομένα, είτε σε περίπτωση σφάλματος ρίχνει την εξαίρεση Nette\Schema\ValidationException.
$processor = new Nette\Schema\Processor;
try {
$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
echo 'Data are not valid: ' . $e->getMessage();
}
Η μέθοδος $e->getMessages()
επιστρέφει ένα array όλων των μηνυμάτων ως
strings και η $e->getMessageObjects()
επιστρέφει όλα τα μηνύματα ως
αντικείμενα Nette\Schema\Message.
Ορισμός του schema
Και τώρα θα δημιουργήσουμε το schema. Για τον ορισμό του χρησιμοποιείται
η κλάση Nette\Schema\Expect, ορίζουμε
στην πραγματικότητα τις προσδοκίες για το πώς πρέπει να μοιάζουν τα
δεδομένα. Ας πούμε ότι τα δεδομένα εισόδου πρέπει να αποτελούν μια δομή
(για παράδειγμα, ένα array) που περιέχει τα στοιχεία processRefund
τύπου
bool και refundAmount
τύπου int.
use Nette\Schema\Expect;
$schema = Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
]);
Πιστεύουμε ότι ο ορισμός του schema φαίνεται κατανοητός, ακόμα κι αν τον βλέπετε για πρώτη φορά.
Θα στείλουμε τα ακόλουθα δεδομένα για επικύρωση:
$data = [
'processRefund' => true,
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, passes validation
Η έξοδος, δηλαδή η τιμή $normalized
, είναι ένα αντικείμενο
stdClass
. Αν θέλαμε η έξοδος να είναι ένα array, θα συμπληρώναμε το schema
με τη μετατροπή τύπου Expect::structure([...])->castTo('array')
.
Όλα τα στοιχεία της δομής είναι προαιρετικά και έχουν προεπιλεγμένη
τιμή null
. Παράδειγμα:
$data = [
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, passes validation
// $normalized = {'processRefund' => null, 'refundAmount' => 17}
Το ότι η προεπιλεγμένη τιμή είναι null
, δεν σημαίνει ότι θα
γινόταν αποδεκτό το 'processRefund' => null
στα δεδομένα εισόδου. Όχι, η
είσοδος πρέπει να είναι boolean, δηλαδή μόνο true
ή false
. Θα
έπρεπε να επιτρέψουμε το null
ρητά με το Expect::bool()->nullable()
.
Ένα στοιχείο μπορεί να καταστεί υποχρεωτικό με το
Expect::bool()->required()
. Αλλάζουμε την προεπιλεγμένη τιμή, για
παράδειγμα, σε false
με το Expect::bool()->default(false)
ή εν συντομία
Expect::bool(false)
.
Και τι θα γινόταν αν θέλαμε να αποδεχτούμε, εκτός από boolean, και τα
1
και 0
; Τότε θα αναφέραμε μια απαρίθμηση τιμών, τις οποίες
επιπλέον θα αφήναμε να κανονικοποιηθούν σε boolean:
$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
Τώρα γνωρίζετε ήδη τα βασικά για το πώς ορίζεται ένα schema και πώς συμπεριφέρονται τα μεμονωμένα στοιχεία της δομής. Τώρα θα δείξουμε ποια άλλα στοιχεία μπορούν να χρησιμοποιηθούν κατά τον ορισμό του schema.
Τύποι δεδομένων: type()
Στο schema μπορείτε να αναφέρετε όλους τους τυπικούς τύπους δεδομένων της PHP:
Expect::string($default = null)
Expect::int($default = null)
Expect::float($default = null)
Expect::bool($default = null)
Expect::null()
Expect::array($default = [])
Και επιπλέον όλους τους τύπους που υποστηρίζονται από την κλάση
Validators, για παράδειγμα Expect::type('scalar')
ή εν συντομία
Expect::scalar()
. Επίσης ονόματα κλάσεων ή interfaces, για παράδειγμα
Expect::type('AddressEntity')
.
Μπορείτε επίσης να χρησιμοποιήσετε τη σύνταξη union:
Expect::type('bool|string|array')
Η προεπιλεγμένη τιμή είναι πάντα null
με εξαίρεση για τα
array
και list
, όπου είναι ένας κενός πίνακας. (Ένα List είναι
ένα array ευρετηριασμένο σύμφωνα με μια αύξουσα σειρά αριθμητικών
κλειδιών από το μηδέν, δηλαδή ένας μη συσχετιστικός πίνακας).
Πίνακες τιμών: arrayOf() listOf()
Ένα Array αντιπροσωπεύει μια πολύ γενική δομή, είναι πιο χρήσιμο να καθορίσετε ποια ακριβώς στοιχεία μπορεί να περιέχει. Για παράδειγμα, ένα array του οποίου τα στοιχεία μπορούν να είναι μόνο strings:
$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 an int
Ένα List είναι ένας ευρετηριασμένος πίνακας:
$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: not a list
$processor->process($schema, [1 => 'a', 0 => 'b']); // ERROR: also not a list
Η παράμετρος μπορεί να είναι και ένα schema, οπότε μπορούμε να γράψουμε:
Expect::arrayOf(Expect::bool())
Η προεπιλεγμένη τιμή είναι ένας κενός πίνακας. Αν καθορίσετε μια
προεπιλεγμένη τιμή, θα συγχωνευθεί με τα παρεχόμενα δεδομένα. Αυτό
μπορεί να απενεργοποιηθεί χρησιμοποιώντας το mergeDefaults(false)
(από
την έκδοση 1.1).
Απαρίθμηση: anyOf()
Το anyOf()
αντιπροσωπεύει μια απαρίθμηση τιμών ή schemas που μπορεί
να λάβει μια τιμή. Έτσι γράφουμε ένα array στοιχείων που μπορούν να είναι
είτε '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
Τα στοιχεία της απαρίθμησης μπορούν να είναι και schemas:
$schema = Expect::listOf(
Expect::anyOf(Expect::string(), true, null),
);
$processor->process($schema, ['foo', true, null, 'bar']); // OK
$processor->process($schema, [123]); // ERROR
Η μέθοδος anyOf()
δέχεται τις παραλλαγές ως μεμονωμένες
παράμετροι, όχι ως array. Αν θέλετε να της περάσετε ένα array τιμών,
χρησιμοποιήστε τον τελεστή unpacking anyOf(...$variants)
.
Η προεπιλεγμένη τιμή είναι null
. Με τη μέθοδο firstIsDefault()
κάνουμε το πρώτο στοιχείο προεπιλογή:
// default is 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();
Δομές
Οι δομές είναι αντικείμενα με ορισμένα κλειδιά. Κάθε ζεύγος κλειδί ⇒ τιμή αναφέρεται ως «ιδιότητα»:
Οι δομές δέχονται arrays και αντικείμενα και επιστρέφουν αντικείμενα
stdClass
.
Από προεπιλογή, όλες οι ιδιότητες είναι προαιρετικές και έχουν
προεπιλεγμένη τιμή null
. Μπορείτε να ορίσετε υποχρεωτικές
ιδιότητες χρησιμοποιώντας το required()
:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // 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'}
Αν και το null
είναι η προεπιλεγμένη τιμή της ιδιότητας
optional
, δεν είναι επιτρεπτό στα δεδομένα εισόδου (η τιμή πρέπει να
είναι string). Ορίζουμε ιδιότητες που δέχονται 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}
Ένα array όλων των ιδιοτήτων της δομής επιστρέφεται από τη μέθοδο
getShape()
.
Από προεπιλογή, δεν μπορούν να υπάρχουν επιπλέον στοιχεία στα δεδομένα εισόδου:
$schema = Expect::structure([
'key' => Expect::string(),
]);
$processor->process($schema, ['additional' => 1]);
// ERROR: Unexpected item 'additional'
Κάτι που μπορούμε να αλλάξουμε χρησιμοποιώντας το otherItems()
. Ως
παράμετρο αναφέρουμε το schema σύμφωνα με το οποίο θα επικυρωθούν τα
επιπλέον στοιχεία:
$schema = Expect::structure([
'key' => Expect::string(),
])->otherItems(Expect::int());
$processor->process($schema, ['additional' => 1]); // OK
$processor->process($schema, ['additional' => true]); // ERROR
Μπορείτε να δημιουργήσετε μια νέα δομή κληρονομώντας από μια άλλη
χρησιμοποιώντας το extend()
:
$dog = Expect::structure([
'name' => Expect::string(),
'age' => Expect::int(),
]);
$dogWithBreed = $dog->extend([
'breed' => Expect::string(),
]);
Πίνακες
Ένα Array με ορισμένα κλειδιά. Ισχύουν για αυτό όλα όσα ισχύουν για τις δομές.
$schema = Expect::array([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // default value is null
]);
Μπορείτε επίσης να ορίσετε έναν ευρετηριασμένο πίνακα, γνωστό ως tuple:
$schema = Expect::array([
Expect::int(),
Expect::string(),
Expect::bool(),
]);
$processor->process($schema, [1, 'hello', true]); // OK
Καταργημένες ιδιότητες
Μπορείτε να επισημάνετε μια ιδιότητα ως deprecated χρησιμοποιώντας τη
μέθοδο 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()
Χρησιμοποιώντας τα min()
και max()
, μπορείτε να περιορίσετε
τον αριθμό των στοιχείων σε arrays:
// array, at least 10 items, at most 20 items
Expect::array()->min(10)->max(20);
Για strings, περιορίστε το μήκος τους:
// string, at least 10 characters long, at most 20 characters
Expect::string()->min(10)->max(20);
Για αριθμούς, περιορίστε την τιμή τους:
// integer, between 10 and 20 inclusive
Expect::int()->min(10)->max(20);
Φυσικά, είναι δυνατόν να αναφέρετε μόνο το min()
, ή μόνο το
max()
:
// string at most 20 characters
Expect::string()->max(20);
Κανονικές εκφράσεις: pattern()
Χρησιμοποιώντας το pattern()
, μπορείτε να αναφέρετε μια regular expression
στην οποία πρέπει να αντιστοιχεί ολόκληρο το string εισόδου (δηλαδή,
σαν να ήταν περιτυλιγμένο με τους χαρακτήρες ^
και $
):
// exactly 9 digits
Expect::string()->pattern('\d{9}');
Προσαρμοσμένοι περιορισμοί: assert()
Οποιουσδήποτε άλλους περιορισμούς εισάγουμε χρησιμοποιώντας το
assert(callable $fn)
.
$countIsEven = fn($v) => count($v) % 2 === 0;
$schema = Expect::arrayOf('string')
->assert($countIsEven); // count must be even
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // ERROR: 3 is not an even count
Ή
Expect::string()->assert('is_file'); // 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()
.
Μετασχηματισμοί: transform()
Τα επιτυχώς επικυρωμένα δεδομένα μπορούν να τροποποιηθούν χρησιμοποιώντας μια προσαρμοσμένη συνάρτηση:
// convert 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)); // convert to uppercase
Η μέθοδος transform()
μπορεί ταυτόχρονα να μετασχηματίσει και να
επικυρώσει την τιμή. Αυτό είναι συχνά απλούστερο και λιγότερο
διπλότυπο από την αλυσίδωση των transform()
και assert()
. Για
αυτόν τον σκοπό, η συνάρτηση λαμβάνει ένα αντικείμενο Context με τη μέθοδο 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);
});
Μετατροπή τύπου: castTo()
Τα επιτυχώς επικυρωμένα δεδομένα μπορούν να μετατραπούν ως προς τον τύπο:
Expect::scalar()->castTo('string');
Εκτός από τους εγγενείς τύπους PHP, μπορείτε να μετατρέψετε τον τύπο και σε κλάσεις. Εδώ διακρίνεται αν πρόκειται για μια απλή κλάση χωρίς κατασκευαστή ή μια κλάση με κατασκευαστή. Αν η κλάση δεν έχει κατασκευαστή, δημιουργείται μια instance της και όλα τα στοιχεία της δομής γράφονται στις properties:
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: ...)
Η μετατροπή τύπου σε συνδυασμό με μια scalar παράμετρο δημιουργεί ένα αντικείμενο και περνά την τιμή ως μοναδική παράμετρο στον κατασκευαστή:
Expect::string()->castTo(DateTime::class);
// creates new DateTime(...)
Κανονικοποίηση: before()
Πριν από την ίδια την επικύρωση, τα δεδομένα μπορούν να
κανονικοποιηθούν χρησιμοποιώντας τη μέθοδο before()
. Ως
παράδειγμα, ας αναφέρουμε ένα στοιχείο που πρέπει να είναι ένα array από
strings (για παράδειγμα ['a', 'b', 'c']
), αλλά δέχεται είσοδο με τη μορφή
του string a b c
:
$explode = fn($v) => explode(' ', $v);
$schema = Expect::arrayOf('string')
->before($explode);
$normalized = $processor->process($schema, 'a b c');
// OK and returns ['a', 'b', 'c']
Αντιστοίχιση σε αντικείμενα: from()
Μπορούμε να αφήσουμε το schema της δομής να παραχθεί από μια κλάση. Παράδειγμα:
class Config
{
public string $name;
public string|null $password;
public bool $admin = false;
}
$schema = Expect::from(new Config);
$data = [
'name' => 'franta',
];
$normalized = $processor->process($schema, $data);
// $normalized instanceof Config
// $normalized = {'name' => 'franta', 'password' => null, 'admin' => false}
Υποστηρίζονται επίσης οι ανώνυμες κλάσεις:
$schema = Expect::from(new class {
public string $name;
public ?string $password;
public bool $admin = false;
});
Επειδή οι πληροφορίες που λαμβάνονται από τον ορισμό της κλάσης μπορεί να μην είναι επαρκείς, μπορείτε να συμπληρώσετε τα στοιχεία με ένα προσαρμοσμένο schema με τη δεύτερη παράμετρο:
$schema = Expect::from(new Config, [
'name' => Expect::string()->pattern('\w:.*'),
]);