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 'Τα δεδομένα δεν είναι έγκυρα: ' . $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, περνάει την επικύρωση

Η έξοδος, δηλαδή η τιμή $normalized, είναι ένα αντικείμενο stdClass. Αν θέλαμε η έξοδος να είναι ένα array, θα συμπληρώναμε το schema με τη μετατροπή τύπου Expect::structure([...])->castTo('array').

Όλα τα στοιχεία της δομής είναι προαιρετικά και έχουν προεπιλεγμένη τιμή null. Παράδειγμα:

$data = [
	'refundAmount' => 17,
];

$normalized = $processor->process($schema, $data); // OK, περνάει την επικύρωση
// $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]); // ΣΦΑΛΜΑ: το 123 δεν είναι string

Με τη δεύτερη παράμετρο μπορείτε να καθορίσετε τα κλειδιά (από την έκδοση 1.2):

$schema = Expect::arrayOf('string', 'int');

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // ΣΦΑΛΜΑ: το 'a' δεν είναι int

Ένα List είναι ένας ευρετηριασμένος πίνακας:

$schema = Expect::listOf('string');

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // ΣΦΑΛΜΑ: το 123 δεν είναι string
$processor->process($schema, ['key' => 'a']); // ΣΦΑΛΜΑ: δεν είναι list
$processor->process($schema, [1 => 'a', 0 => 'b']); // ΣΦΑΛΜΑ: επίσης δεν είναι 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]); // ΣΦΑΛΜΑ: το false δεν ανήκει εκεί

Τα στοιχεία της απαρίθμησης μπορούν να είναι και schemas:

$schema = Expect::listOf(
	Expect::anyOf(Expect::string(), true, null),
);

$processor->process($schema, ['foo', true, null, 'bar']); // OK
$processor->process($schema, [123]); // ΣΦΑΛΜΑ

Η μέθοδος anyOf() δέχεται τις παραλλαγές ως μεμονωμένες παράμετροι, όχι ως array. Αν θέλετε να της περάσετε ένα array τιμών, χρησιμοποιήστε τον τελεστή unpacking anyOf(...$variants).

Η προεπιλεγμένη τιμή είναι null. Με τη μέθοδο firstIsDefault() κάνουμε το πρώτο στοιχείο προεπιλογή:

// η προεπιλογή είναι 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();

Δομές

Οι δομές είναι αντικείμενα με ορισμένα κλειδιά. Κάθε ζεύγος κλειδί ⇒ τιμή αναφέρεται ως «ιδιότητα»:

Οι δομές δέχονται arrays και αντικείμενα και επιστρέφουν αντικείμενα stdClass.

Από προεπιλογή, όλες οι ιδιότητες είναι προαιρετικές και έχουν προεπιλεγμένη τιμή null. Μπορείτε να ορίσετε υποχρεωτικές ιδιότητες χρησιμοποιώντας το required():

$schema = Expect::structure([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // η προεπιλεγμένη τιμή είναι null
]);

$processor->process($schema, ['optional' => '']);
// ΣΦΑΛΜΑ: η επιλογή 'required' λείπει

$processor->process($schema, ['required' => 'foo']);
// OK, επιστρέφει {'required' => 'foo', 'optional' => null}

Αν δεν θέλετε να έχετε στην έξοδο ιδιότητες με προεπιλεγμένη τιμή, χρησιμοποιήστε το skipDefaults():

$schema = Expect::structure([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(),
])->skipDefaults();

$processor->process($schema, ['required' => 'foo']);
// OK, επιστρέφει {'required' => 'foo'}

Αν και το null είναι η προεπιλεγμένη τιμή της ιδιότητας optional, δεν είναι επιτρεπτό στα δεδομένα εισόδου (η τιμή πρέπει να είναι string). Ορίζουμε ιδιότητες που δέχονται null χρησιμοποιώντας το nullable():

$schema = Expect::structure([
	'optional' => Expect::string(),
	'nullable' => Expect::string()->nullable(),
]);

$processor->process($schema, ['optional' => null]);
// ΣΦΑΛΜΑ: το 'optional' αναμένεται να είναι string, δόθηκε null.

$processor->process($schema, ['nullable' => null]);
// OK, επιστρέφει {'optional' => null, 'nullable' => null}

Ένα array όλων των ιδιοτήτων της δομής επιστρέφεται από τη μέθοδο getShape().

Από προεπιλογή, δεν μπορούν να υπάρχουν επιπλέον στοιχεία στα δεδομένα εισόδου:

$schema = Expect::structure([
	'key' => Expect::string(),
]);

$processor->process($schema, ['additional' => 1]);
// ΣΦΑΛΜΑ: Μη αναμενόμενο στοιχείο 'additional'

Κάτι που μπορούμε να αλλάξουμε χρησιμοποιώντας το otherItems(). Ως παράμετρο αναφέρουμε το schema σύμφωνα με το οποίο θα επικυρωθούν τα επιπλέον στοιχεία:

$schema = Expect::structure([
	'key' => Expect::string(),
])->otherItems(Expect::int());

$processor->process($schema, ['additional' => 1]); // OK
$processor->process($schema, ['additional' => true]); // ΣΦΑΛΜΑ

Μπορείτε να δημιουργήσετε μια νέα δομή κληρονομώντας από μια άλλη χρησιμοποιώντας το 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(), // η προεπιλεγμένη τιμή είναι 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('Το στοιχείο %path% έχει καταργηθεί'),
]);

$processor->process($schema, ['old' => 1]); // OK
$processor->getWarnings(); // ["Το στοιχείο 'old' έχει καταργηθεί"]

Εύρη: min() max()

Χρησιμοποιώντας τα min() και max(), μπορείτε να περιορίσετε τον αριθμό των στοιχείων σε arrays:

// array, τουλάχιστον 10 στοιχεία, το πολύ 20 στοιχεία
Expect::array()->min(10)->max(20);

Για strings, περιορίστε το μήκος τους:

// string, τουλάχιστον 10 χαρακτήρες μήκος, το πολύ 20 χαρακτήρες
Expect::string()->min(10)->max(20);

Για αριθμούς, περιορίστε την τιμή τους:

// ακέραιος, μεταξύ 10 και 20 συμπεριλαμβανομένων
Expect::int()->min(10)->max(20);

Φυσικά, είναι δυνατόν να αναφέρετε μόνο το min(), ή μόνο το max():

// string το πολύ 20 χαρακτήρες
Expect::string()->max(20);

Κανονικές εκφράσεις: pattern()

Χρησιμοποιώντας το pattern(), μπορείτε να αναφέρετε μια regular expression στην οποία πρέπει να αντιστοιχεί ολόκληρο το string εισόδου (δηλαδή, σαν να ήταν περιτυλιγμένο με τους χαρακτήρες ^ και $):

// ακριβώς 9 ψηφία
Expect::string()->pattern('\d{9}');

Προσαρμοσμένοι περιορισμοί: assert()

Οποιουσδήποτε άλλους περιορισμούς εισάγουμε χρησιμοποιώντας το assert(callable $fn).

$countIsEven = fn($v) => count($v) % 2 === 0;

$schema = Expect::arrayOf('string')
	->assert($countIsEven); // ο αριθμός πρέπει να είναι ζυγός

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // ΣΦΑΛΜΑ: το 3 δεν είναι ζυγός αριθμός

Ή

Expect::string()->assert('is_file'); // το αρχείο πρέπει να υπάρχει

Σε κάθε περιορισμό μπορείτε να προσθέσετε μια προσαρμοσμένη περιγραφή. Αυτή θα είναι μέρος του μηνύματος σφάλματος.

$schema = Expect::arrayOf('string')
	->assert($countIsEven, 'Ζυγός αριθμός στοιχείων στον πίνακα');

$processor->process($schema, ['a', 'b', 'c']);
// Failed assertion "Ζυγός αριθμός στοιχείων στον πίνακα" for item with value array.

Η μέθοδος μπορεί να κληθεί επανειλημμένα για να προσθέσετε περισσότερους περιορισμούς. Μπορεί να εναλλάσσεται με κλήσεις των transform() και castTo().

Μετασχηματισμοί: transform()

Τα επιτυχώς επικυρωμένα δεδομένα μπορούν να τροποποιηθούν χρησιμοποιώντας μια προσαρμοσμένη συνάρτηση:

// μετατροπή σε κεφαλαία:
Expect::string()->transform(fn(string $s) => strtoupper($s));

Η μέθοδος μπορεί να κληθεί επανειλημμένα για να προσθέσετε περισσότερους μετασχηματισμούς. Μπορεί να εναλλάσσεται με κλήσεις των assert() και castTo(). Οι λειτουργίες εκτελούνται με τη σειρά που δηλώνονται:

Expect::type('string|int')
	->castTo('string')
	->assert('ctype_lower', 'Όλοι οι χαρακτήρες πρέπει να είναι πεζοί')
	->transform(fn(string $s) => strtoupper($s)); // μετατροπή σε κεφαλαία

Η μέθοδος transform() μπορεί ταυτόχρονα να μετασχηματίσει και να επικυρώσει την τιμή. Αυτό είναι συχνά απλούστερο και λιγότερο διπλότυπο από την αλυσίδωση των transform() και assert(). Για αυτόν τον σκοπό, η συνάρτηση λαμβάνει ένα αντικείμενο Context με τη μέθοδο addError(), η οποία μπορεί να χρησιμοποιηθεί για την προσθήκη πληροφοριών σχετικά με προβλήματα επικύρωσης:

Expect::string()
	->transform(function (string $s, Nette\Schema\Context $context) {
		if (!ctype_lower($s)) {
			$context->addError('Όλοι οι χαρακτήρες πρέπει να είναι πεζοί', 'my.case.error');
			return null;
		}

		return strtoupper($s);
	});

Μετατροπή τύπου: castTo()

Τα επιτυχώς επικυρωμένα δεδομένα μπορούν να μετατραπούν ως προς τον τύπο:

Expect::scalar()->castTo('string');

Εκτός από τους εγγενείς τύπους PHP, μπορείτε να μετατρέψετε τον τύπο και σε κλάσεις. Εδώ διακρίνεται αν πρόκειται για μια απλή κλάση χωρίς κατασκευαστή ή μια κλάση με κατασκευαστή. Αν η κλάση δεν έχει κατασκευαστή, δημιουργείται μια instance της και όλα τα στοιχεία της δομής γράφονται στις ιδιότητες:

class Info
{
	public bool $processRefund;
	public int $refundAmount;
}

Expect::structure([
	'processRefund' => Expect::bool(),
	'refundAmount' => Expect::int(),
])->castTo(Info::class);

// δημιουργεί '$obj = new Info' και γράφει στα $obj->processRefund και $obj->refundAmount

Αν η κλάση έχει κατασκευαστή, τα στοιχεία της δομής περνούν ως ονομασμένες παράμετροι στον κατασκευαστή:

class Info
{
	public function __construct(
		public bool $processRefund,
		public int $refundAmount,
	) {
	}
}

// δημιουργεί $obj = new Info(processRefund: ..., refundAmount: ...)

Η μετατροπή τύπου σε συνδυασμό με μια scalar παράμετρο δημιουργεί ένα αντικείμενο και περνά την τιμή ως μοναδική παράμετρο στον κατασκευαστή:

Expect::string()->castTo(DateTime::class);
// δημιουργεί 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 και επιστρέφει ['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:.*'),
]);
έκδοση: 2.0