Σχήμα: Επικύρωση δεδομένων

Μια πρακτική βιβλιοθήκη για την επικύρωση και την κανονικοποίηση δομών δεδομένων έναντι ενός δεδομένου σχήματος με ένα έξυπνο και κατανοητό API.

Εγκατάσταση:

composer require nette/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 is invalid: ' . $e->getMessage();
}

Η μέθοδος $e->getMessages() επιστρέφει πίνακα όλων των συμβολοσειρών μηνυμάτων και η $e->getMessageObjects() επιστρέφει όλα τα μηνύματα ως αντικείμενα Nette\Schema\Message.

Ορισμός σχήματος

Και τώρα ας δημιουργήσουμε ένα σχήμα. Η κλάση Nette\Schema\Expect χρησιμοποιείται για τον ορισμό του, ουσιαστικά ορίζουμε τις προσδοκίες για το πώς θα πρέπει να μοιάζουν τα δεδομένα. Ας πούμε ότι τα δεδομένα εισόδου πρέπει να είναι μια δομή (π.χ. ένας πίνακας) που περιέχει στοιχεία 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); // Εντάξει, περνάει

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

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

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

$normalized = $processor->process($schema, $data); // Εντάξει, περνάει
// $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).

Και τι θα γινόταν αν θέλαμε να δεχτούμε το 1 and 0 εκτός από booleans; Τότε απαριθμούμε τις επιτρεπόμενες τιμές, τις οποίες επίσης θα κανονικοποιήσουμε σε 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

Τώρα γνωρίζετε τα βασικά για το πώς ορίζεται το σχήμα και πώς συμπεριφέρονται τα επιμέρους στοιχεία της δομής. Τώρα θα δείξουμε ποια είναι όλα τα άλλα στοιχεία που μπορούν να χρησιμοποιηθούν στον ορισμό ενός σχήματος.

Τύποι δεδομένων: type()

Όλοι οι τυποποιημένοι τύποι δεδομένων 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, όπου είναι ένας άδειος πίνακας. (Μια λίστα είναι ένας πίνακας με ευρετηρίαση σε αύξουσα σειρά αριθμητικών κλειδιών από το μηδέν, δηλαδή ένας μη συσχετιστικός πίνακας).

Πίνακας τιμών: arrayOf() listOf()

Ο πίνακας είναι πολύ γενική δομή, είναι πιο χρήσιμο να καθορίσετε ακριβώς ποια στοιχεία μπορεί να περιέχει. Για παράδειγμα, ένας πίνακας του οποίου τα στοιχεία μπορούν να είναι μόνο συμβολοσειρές:

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

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // ΣΦΑΛΜΑ: Το 123 δεν είναι συμβολοσειρά

Η δεύτερη παράμετρος μπορεί να χρησιμοποιηθεί για τον προσδιορισμό των κλειδιών (από την έκδοση 1.2):

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

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

Η λίστα είναι ένας δεικτοδοτημένος πίνακας:

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

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // ΣΦΑΛΜΑ: Το 123 δεν είναι συμβολοσειρά
$processor->process($schema, ['key' => 'a']); // ΣΦΑΛΜΑ: δεν είναι λίστα
$processor->process($schema, [1 => 'a', 0 => 'b']); // ΣΦΑΛΜΑ: δεν είναι λίστα

Η παράμετρος μπορεί επίσης να είναι ένα σχήμα, οπότε μπορούμε να γράψουμε:

Expect::arrayOf(Expect::bool())

Η προεπιλεγμένη τιμή είναι ένας άδειος πίνακας. Εάν καθορίσετε την προεπιλεγμένη τιμή, θα συγχωνευτεί με τα δεδομένα που έχουν περάσει. Αυτό μπορεί να απενεργοποιηθεί χρησιμοποιώντας το mergeDefaults(false) (από την έκδοση 1.1).

Απαρίθμηση: anyOf()

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]); // ΣΦΑΛΜΑ: το false δεν ανήκει εκεί

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

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

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

Η μέθοδος anyOf() δέχεται παραλλαγές ως μεμονωμένες παραμέτρους, όχι ως πίνακα. Για να της παραδώσετε έναν πίνακα τιμών, χρησιμοποιήστε τον τελεστή αποσυμπίεσης anyOf(...$variants).

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

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

Δομές

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

Οι δομές δέχονται πίνακες και αντικείμενα και επιστρέφουν αντικείμενα stdClass (εκτός αν το αλλάξετε με το castTo('array'), κ.λπ.).

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

$schema = Expect::structure([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // η προεπιλεγμένη τιμή είναι 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, επιστρέφει {'required' => 'foo'}

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

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

$processor->process($schema, ['optional' => null]);
// ERROR: 'optional' αναμένει να είναι string, null given.

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

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

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

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

Το οποίο μπορούμε να αλλάξουμε με το otherItems(). Ως παράμετρος, θα καθορίσουμε το σχήμα για κάθε επιπλέον στοιχείο:

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

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

Αποσβέσεις

Μπορείτε να καταργήσετε μια ιδιότητα χρησιμοποιώντας την εντολή 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() για να περιορίσετε τον αριθμό των στοιχείων των πινάκων:

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

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

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

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

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

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

// συμβολοσειρά, μέγιστο 20 χαρακτήρες
Expect::string()->max(20);

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

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

// μόλις 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, 'Even items in array');

$processor->process($schema, ['a', 'b', 'c']);
// Αποτυχημένος ισχυρισμός "Even items in array" για στοιχείο με τιμή array.

Η μέθοδος μπορεί να κληθεί επανειλημμένα για την προσθήκη πολλαπλών περιορισμών. Μπορεί να αναμειχθεί με κλήσεις στις transform() και castTo().

Μετασχηματισμός: transform()

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

// 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(). Για το σκοπό αυτό, η συνάρτηση λαμβάνει ένα αντικείμενο 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);
	});

Casting: castTo()

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

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

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

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()

Πριν από την ίδια την επικύρωση, τα δεδομένα μπορούν να κανονικοποιηθούν χρησιμοποιώντας τη μέθοδο before(). Ως παράδειγμα, ας έχουμε ένα στοιχείο που πρέπει να είναι ένας πίνακας συμβολοσειρών (π.χ. ['a', 'b', 'c']), αλλά δέχεται είσοδο με τη μορφή συμβολοσειράς 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()

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

class Config
{
	public string $name;
	public string|null $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:.*'),
]);
έκδοση: 2.0