Schema: Veri Doğrulama

Akıllı, anlaşılır bir API ile belirli bir şemaya göre veri yapılarının doğrulanması ve normalleştirilmesi için pratik bir kütüphane.

Kurulum:

composer require nette/schema

Temel Kullanım

$schema değişkeninde doğrulama şemamız var (bunun tam olarak ne anlama geldiğini ve böyle bir şemanın nasıl oluşturulacağını birazdan açıklayacağız) ve $data değişkeninde doğrulamak ve normalleştirmek istediğimiz veri yapısı var. Bu, örneğin bir API arayüzü aracılığıyla kullanıcı tarafından gönderilen veriler, bir yapılandırma dosyası vb. olabilir.

Görevi, girişi işleyen ve ya normalleştirilmiş verileri döndüren ya da bir hata durumunda bir Nette\Schema\ValidationException istisnası fırlatan Nette\Schema\Processor sınıfı üstlenir.

$processor = new Nette\Schema\Processor;

try {
	$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
	echo 'Veriler geçerli değil: ' . $e->getMessage();
}

$e->getMessages() metodu tüm mesajların dizisini dizeler olarak döndürür ve $e->getMessageObjects() tüm mesajları Nette\Schema\Message nesneleri olarak döndürür.

Şema Tanımlama

Ve şimdi şemayı oluşturalım. Tanımlamak için Nette\Schema\Expect sınıfı kullanılır, aslında verilerin nasıl görünmesi gerektiğine dair beklentileri tanımlarız. Giriş verilerinin, processRefund türünde bool ve refundAmount türünde int öğeleri içeren bir yapı (örneğin bir dizi) oluşturması gerektiğini varsayalım.

use Nette\Schema\Expect;

$schema = Expect::structure([
	'processRefund' => Expect::bool(),
	'refundAmount' => Expect::int(),
]);

Şema tanımının, ilk kez görseniz bile anlaşılır göründüğüne inanıyoruz.

Doğrulama için aşağıdaki verileri gönderelim:

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

$normalized = $processor->process($schema, $data); // OK, doğrulamadan geçer

Çıktı, yani $normalized değeri, bir stdClass nesnesidir. Çıktının bir dizi olmasını isteseydik, şemayı Expect::structure([...])->castTo('array') tür dönüşümüyle tamamlardık.

Yapının tüm öğeleri isteğe bağlıdır ve varsayılan değeri null'dur. Örnek:

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

$normalized = $processor->process($schema, $data); // OK, doğrulamadan geçer
// $normalized = {'processRefund' => null, 'refundAmount' => 17}

Varsayılan değerin null olması, giriş verilerinde 'processRefund' => null'ın kabul edileceği anlamına gelmez. Hayır, giriş bir boolean olmalıdır, yani yalnızca true veya false. null'a izin vermek için bunu Expect::bool()->nullable() ile açıkça belirtmemiz gerekirdi.

Bir öğeyi Expect::bool()->required() ile zorunlu kılabiliriz. Varsayılan değeri örneğin Expect::bool()->default(false) ile false olarak veya kısaca Expect::bool(false) ile değiştirebiliriz.

Peki ya boolean'a ek olarak 1 ve 0'ı da kabul etmek isteseydik? O zaman ayrıca boolean'a normalleştirilmesini sağlayacağımız değerlerin bir listesini belirtiriz:

$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

Artık şemanın nasıl tanımlandığının ve yapının bireysel öğelerinin nasıl davrandığının temellerini biliyorsunuz. Şimdi şema tanımlarken kullanılabilecek diğer tüm öğeleri göstereceğiz.

Veri Tipleri: type()

Şemada tüm standart PHP veri tipleri belirtilebilir:

Expect::string($default = null)
Expect::int($default = null)
Expect::float($default = null)
Expect::bool($default = null)
Expect::null()
Expect::array($default = [])

Ve ayrıca Validators sınıfı tarafından desteklenen tüm türler, örneğin Expect::type('scalar') veya kısaca Expect::scalar(). Ayrıca sınıf veya arayüz adları, örneğin Expect::type('AddressEntity').

Union gösterimi de kullanılabilir:

Expect::type('bool|string|array')

Varsayılan değer, boş bir dizi olduğu array ve list hariç her zaman null'dur. (Liste, sıfırdan başlayarak artan bir dizi sayısal anahtara göre indekslenmiş bir dizidir, yani ilişkisel olmayan bir dizi).

Değer Dizileri: arrayOf() listOf()

Dizi çok genel bir yapıdır, tam olarak hangi öğeleri içerebileceğini belirtmek daha kullanışlıdır. Örneğin, öğeleri yalnızca dizeler olabilen bir dizi:

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

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // HATA: 123 bir dize değil

İkinci parametre ile anahtarlar belirtilebilir (sürüm 1.2'den itibaren):

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

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // HATA: 'a' bir int değil

Liste, indekslenmiş bir dizidir:

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

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // HATA: 123 bir dize değil
$processor->process($schema, ['key' => 'a']); // HATA: liste değil
$processor->process($schema, [1 => 'a', 0 => 'b']); // HATA: ayrıca liste değil

Parametre bir şema da olabilir, bu yüzden yazabiliriz:

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

Varsayılan değer boş bir dizidir. Bir varsayılan değer belirtirseniz, iletilen verilerle birleştirilecektir. Bu, mergeDefaults(false) ile devre dışı bırakılabilir (sürüm 1.1'den itibaren).

Enum: anyOf()

anyOf(), bir değerin alabileceği değerlerin veya şemaların bir listesini temsil eder. Bu şekilde, öğeleri 'a', true veya null olabilen bir öğe dizisi yazarız:

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

$processor->process($schema, ['a', true, null, 'a']); // OK
$processor->process($schema, ['a', false]); // HATA: false oraya ait değil

Enum öğeleri şemalar da olabilir:

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

$processor->process($schema, ['foo', true, null, 'bar']); // OK
$processor->process($schema, [123]); // HATA

anyOf() metodu, varyantları bir dizi olarak değil, bireysel parametreler olarak kabul eder. Ona bir değer dizisi iletmek istiyorsanız, unpacking operatörünü anyOf(...$variants) kullanın.

Varsayılan değer null'dur. firstIsDefault() metoduyla ilk öğeyi varsayılan yaparız:

// varsayılan 'hello'dur
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();

Yapılar

Yapılar, tanımlanmış anahtarlara sahip nesnelerdir. Her anahtar ⇒ değer çifti “özellik” olarak adlandırılır:

Yapılar dizileri ve nesneleri kabul eder ve stdClass nesneleri döndürür.

Varsayılan olarak, tüm özellikler isteğe bağlıdır ve varsayılan değeri null'dur. Zorunlu özellikleri required() ile tanımlayabilirsiniz:

$schema = Expect::structure([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // varsayılan değer null'dur
]);

$processor->process($schema, ['optional' => '']);
// HATA: 'required' seçeneği eksik

$processor->process($schema, ['required' => 'foo']);
// OK, {'required' => 'foo', 'optional' => null} döndürür

Çıktıda varsayılan değere sahip özellikleri istemiyorsanız, skipDefaults() kullanın:

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

$processor->process($schema, ['required' => 'foo']);
// OK, {'required' => 'foo'} döndürür

null, optional özelliğinin varsayılan değeri olmasına rağmen, giriş verilerinde izin verilmez (değer bir dize olmalıdır). null kabul eden özellikleri nullable() ile tanımlarız:

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

$processor->process($schema, ['optional' => null]);
// HATA: 'optional' dize olması bekleniyor, null verildi.

$processor->process($schema, ['nullable' => null]);
// OK, {'optional' => null, 'nullable' => null} döndürür

Yapının tüm özelliklerinin dizisini getShape() metodu döndürür.

Varsayılan olarak, giriş verilerinde ekstra öğe olamaz:

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

$processor->process($schema, ['additional' => 1]);
// HATA: Beklenmeyen öğe 'additional'

Bunu otherItems() ile değiştirebiliriz. Parametre olarak, ekstra öğelerin doğrulanacağı şemayı belirtiriz:

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

$processor->process($schema, ['additional' => 1]); // OK
$processor->process($schema, ['additional' => true]); // HATA

extend() kullanarak başka bir yapıdan türeterek yeni bir yapı oluşturabilirsiniz:

$dog = Expect::structure([
	'name' => Expect::string(),
	'age' => Expect::int(),
]);

$dogWithBreed = $dog->extend([
	'breed' => Expect::string(),
]);

Diziler

Tanımlanmış anahtarlara sahip dizi. Yapılar için geçerli olan her şey onun için de geçerlidir.

$schema = Expect::array([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // varsayılan değer null'dur
]);

Tuple olarak bilinen indekslenmiş bir dizi de tanımlanabilir:

$schema = Expect::array([
	Expect::int(),
	Expect::string(),
	Expect::bool(),
]);

$processor->process($schema, [1, 'hello', true]); // OK

Kullanımdan Kaldırılmış Özellikler

Bir özelliği deprecated([string $message]) metoduyla kullanımdan kaldırılmış olarak işaretleyebilirsiniz. Destek sonu bilgileri $processor->getWarnings() ile döndürülür:

$schema = Expect::structure([
	'old' => Expect::int()->deprecated('Öğe %path% kullanımdan kaldırılmıştır'),
]);

$processor->process($schema, ['old' => 1]); // OK
$processor->getWarnings(); // ["Öğe 'old' kullanımdan kaldırılmıştır"]

Aralıklar: min() max()

min() ve max() kullanarak dizilerdeki öğe sayısını sınırlayabiliriz:

// dizi, en az 10 öğe, en fazla 20 öğe
Expect::array()->min(10)->max(20);

Dizeler için uzunluklarını sınırlayabiliriz:

// dize, en az 10 karakter uzunluğunda, en fazla 20 karakter
Expect::string()->min(10)->max(20);

Sayılar için değerlerini sınırlayabiliriz:

// tam sayı, 10 ile 20 arasında (dahil)
Expect::int()->min(10)->max(20);

Elbette, yalnızca min() veya yalnızca max() belirtmek mümkündür:

// en fazla 20 karakterlik dize
Expect::string()->max(20);

Düzenli İfadeler: pattern()

pattern() kullanarak, tüm giriş dizesinin eşleşmesi gereken bir düzenli ifade belirtebiliriz (yani, ^ ve $ karakterleriyle sarılmış gibi):

// tam olarak 9 rakam
Expect::string()->pattern('\d{9}');

Özel Kısıtlamalar: assert()

Diğer tüm kısıtlamaları assert(callable $fn) kullanarak belirtebiliriz.

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

$schema = Expect::arrayOf('string')
	->assert($countIsEven); // sayı çift olmalıdır

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // HATA: 3 çift sayı değil

Veya

Expect::string()->assert('is_file'); // dosya mevcut olmalıdır

Her kısıtlamaya kendi açıklamanızı ekleyebilirsiniz. Bu, hata mesajının bir parçası olacaktır.

$schema = Expect::arrayOf('string')
	->assert($countIsEven, 'Dizideki çift öğeler');

$processor->process($schema, ['a', 'b', 'c']);
// Değeri dizi olan öğe için "Dizideki çift öğeler" iddiası başarısız oldu.

Metot tekrar tekrar çağrılarak daha fazla kısıtlama eklenebilir. transform() ve castTo() çağrılarıyla serpiştirilebilir.

Dönüşümler: transform()

Başarıyla doğrulanmış veriler özel bir fonksiyon kullanılarak değiştirilebilir:

// büyük harfe dönüştürme:
Expect::string()->transform(fn(string $s) => strtoupper($s));

Metot tekrar tekrar çağrılarak daha fazla dönüşüm eklenebilir. assert() ve castTo() çağrılarıyla serpiştirilebilir. İşlemler, bildirildikleri sırayla gerçekleştirilir:

Expect::type('string|int')
	->castTo('string')
	->assert('ctype_lower', 'Tüm karakterler küçük harf olmalıdır')
	->transform(fn(string $s) => strtoupper($s)); // büyük harfe dönüştürme

transform() metodu aynı anda değeri hem dönüştürebilir hem de doğrulayabilir. Bu genellikle transform() ve assert() zincirlemesinden daha basit ve daha az tekrarlıdır. Bu amaçla, fonksiyon, doğrulama sorunları hakkında bilgi eklemek için kullanılabilecek addError() metoduna sahip bir Context nesnesi alır:

Expect::string()
	->transform(function (string $s, Nette\Schema\Context $context) {
		if (!ctype_lower($s)) {
			$context->addError('Tüm karakterler küçük harf olmalıdır', 'my.case.error');
			return null;
		}

		return strtoupper($s);
	});

Tür Dönüştürme: castTo()

Başarıyla doğrulanmış veriler tür dönüştürülebilir:

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

Yerel PHP türlerine ek olarak, sınıflara da tür dönüştürme yapılabilir. Bu, basit bir yapıcı olmayan sınıf mı yoksa yapıcı olan bir sınıf mı olduğuna bağlıdır. Sınıfın bir yapıcısı yoksa, örneği oluşturulur ve yapının tüm öğeleri özelliklere yazılır:

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

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

// '$obj = new Info' oluşturur ve $obj->processRefund ve $obj->refundAmount'a yazar

Sınıfın bir yapıcısı varsa, yapının öğeleri yapıcıya adlandırılmış parametreler olarak iletilir:

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

// $obj = new Info(processRefund: ..., refundAmount: ...) oluşturur

Skaler bir parametre ile birleştirilmiş tür dönüştürme, bir nesne oluşturur ve değeri yapıcıya tek bir parametre olarak iletir:

Expect::string()->castTo(DateTime::class);
// new DateTime(...) oluşturur

Normalizasyon: before()

Doğrulamadan önce veriler before() metodu kullanılarak normalleştirilebilir. Örnek olarak, dize dizisi olması gereken (örneğin ['a', 'b', 'c']) ancak a b c dizesi biçiminde girdi kabul eden bir öğeyi ele alalım:

$explode = fn($v) => explode(' ', $v);

$schema = Expect::arrayOf('string')
	->before($explode);

$normalized = $processor->process($schema, 'a b c');
// OK ve ['a', 'b', 'c'] döndürür

Nesnelere Eşleme: from()

Yapı şemasını bir sınıftan oluşturabiliriz. Örnek:

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}

Anonim sınıflar da desteklenir:

$schema = Expect::from(new class {
	public string $name;
	public ?string $password;
	public bool $admin = false;
});

Sınıf tanımından elde edilen bilgiler yeterli olmayabileceğinden, ikinci parametre ile öğelere kendi şemanızı ekleyebilirsiniz:

$schema = Expect::from(new Config, [
	'name' => Expect::string()->pattern('\w:.*'),
]);
versiyon: 2.0