Nette Schema
Veri yapılarını verilen bir şemaya karşı doğrulamak ve normalleştirmek için, akıllı ve anlaşılması kolay bir API sunan pratik bir kütüphane.
Kurulum:
composer require nette/schema
Temel Kullanım
$schema değişkeninde bir doğrulama şemamız var (bunun ne demek olduğunu ve nasıl oluşturulacağını
birazdan açıklayacağız), $data değişkeninde ise doğrulamak ve normalleştirmek istediğimiz veri yapısı var.
Bu örneğin bir kullanıcının API üzerinden gönderdiği veri, bir yapılandırma dosyası vb. olabilir.
İşi Nette\Schema\Processor sınıfı üstlenir; o, girdiyi işler ve ya normalleştirilmiş veriyi döndürür ya da bir hata olursa Nette\Schema\ValidationException istisnası fırlatır.
$processor = new Nette\Schema\Processor;
try {
$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
echo 'Data is invalid: ' . $e->getMessage();
}
$e->getMessages() metodu tüm mesajları dize olarak içeren bir dizi, $e->getMessageObjects()
ise tüm mesajları Nette\Schema\Message nesneleri
olarak döndürür.
Şemayı Tanımlama
Ve şimdi şemayı oluşturalım. Onu tanımlamak için Nette\Schema\Expect sınıfı kullanılır; özünde
verinin nasıl görünmesi gerektiğine ilişkin beklentileri tanımlarız. Diyelim ki girdi verisi, bool tipinde
processRefund ve int tipinde refundAmount öğelerini içeren bir yapı (örneğin bir dizi)
olmalı.
use Nette\Schema\Expect;
$schema = Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
]);
Şema tanımının, onu ilk kez görüyor olsanız bile anlaşılır göründüğüne inanıyoruz.
Doğrulama için şu veriyi gönderelim:
$data = [
'processRefund' => true,
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, doğrulamayı geçer
Çıktı, yani $normalized değeri bir stdClass nesnesidir. Çıktının bir dizi olmasını
isteseydik, şemaya ->castTo('array') dönüşümünü eklerdik.
Yapının tüm öğeleri isteğe bağlıdır ve varsayılan değerleri null olur. Örnek:
$data = [
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, doğrulamayı geçer
// $normalized = {'processRefund' => null, 'refundAmount' => 17}
Varsayılan değerin null olması, girdi verisinde 'processRefund' => null değerini kabul
edeceği anlamına gelmez. Hayır, girdi bir mantıksal değer, yani yalnızca true ya da false
olmalıdır. null değerine açıkça izin vermek için Expect::bool()->nullable() kullanmamız
gerekirdi.
Bir öğe, Expect::bool()->required() kullanılarak zorunlu kılınabilir. Varsayılan değeri örneğin
false olarak Expect::bool()->default(false) ile ya da kısaca Expect::bool(false) ile
değiştirebiliriz.
Peki mantıksal değerlerin yanı sıra 1 ve 0 değerlerini de kabul etmek isteseydik? O zaman
mantıksal değere normalleştirmek istediğimiz değerleri listeleriz:
$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 bir şema tanımlamanın temellerini ve yapı öğelerinin nasıl davrandığını biliyorsunuz. Şimdi bir şema tanımlarken başka hangi öğeleri kullanabileceğinizi 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 = [])
Expect::list($default = [])
Ayrıca Validators sınıfının desteklediği
tüm tipler de, örneğin Expect::type('scalar') ya da kısaca Expect::scalar(). Sınıf ya da arayüz
adları da, örneğin Expect::type('AddressEntity').
Union sözdizimi de kullanılabilir:
Expect::type('bool|string|array')
Varsayılan değer, boş dizi olduğu array ve list dışında her zaman null
değeridir. (Liste, sıfırdan başlayan sayısal anahtarlar dizisiyle indekslenen bir dizidir, yani ilişkisel olmayan
bir dizi.)
Değer Dizisi: arrayOf() listOf()
Dizi çok genel bir yapıyı temsil eder; hangi öğeleri içerebileceğini tam olarak belirtmek daha yararlıdır. Örneğin öğeleri yalnızca dize 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 anahtarları belirtebilir (1.2 sürümünden beri):
$schema = Expect::arrayOf('string', 'int');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // HATA: 'a' bir int değil
Liste, indeksli 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: bu da liste değil
Parametre bir şema da olabilir, dolayısıyla şöyle yazabiliriz:
Expect::arrayOf(Expect::bool())
Varsayılan değer boş bir dizidir. Bir varsayılan değer belirtirseniz, o aktarılan veriyle birleştirilir. Bu,
mergeDefaults(false) ile kapatılabilir (1.1 sürümünden beri).
Sıralama: anyOf()
anyOf(), bir değerin alabileceği değerlerden ya da şemalardan oluşan bir kümeyi temsil eder. Öğeleri
'a', true ya da null olabilen bir diziyi şöyle yazarsını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
Sıralamanın öğeleri şema 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 çeşitleri bir dizi olarak değil, ayrı parametreler olarak kabul eder. Ona bir değer dizisi
aktarmak için anyOf(...$variants) unpack operatörünü kullanın.
Varsayılan değer null değeridir. İlk öğeyi varsayılan yapmak için firstIsDefault() metodunu
kullanın:
// varsayılan 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();
Yapılar
Yapılar, tanımlı anahtarları olan nesnelerdir. Her anahtar-değer çiftine “özellik” denir.
Yapılar dizileri ve nesneleri kabul eder, stdClass nesneleri döndürür.
Varsayılan olarak tüm özellikler isteğe bağlıdır ve varsayılan değerleri null olur. Zorunlu özellikleri
required() kullanarak tanımlayabilirsiniz:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // varsayılan değer null
]);
$processor->process($schema, ['optional' => '']);
// HATA: 'required' seçeneği eksik
$processor->process($schema, ['required' => 'foo']);
// OK, {'required' => 'foo', 'optional' => null} döndürür
Bir yapının kendisi zorunludur. Bu yüzden başka bir yapının içine gömülüyse ve girdi onu içermiyorsa, yine de
oluşturulur ve zorunlu bir özellik içerdiğinde hata bildirir. İç içe yapının tamamını isteğe bağlı kılmak için
required(false) kullanın. Girdide eksikse çıktıda null görünür, ama varsa zorunlu özellikleri
zorunlu tutulur:
$schema = Expect::structure([
'db' => Expect::structure([
'dsn' => Expect::string()->required(),
])->required(false),
]);
$processor->process($schema, []);
// OK, {'db' => null} döndürür
$processor->process($schema, ['db' => []]);
// HATA: 'db › dsn' eksik
Varsayılan değere sahip özelliklerin çıktıda olmasını 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
optional özelliğinin varsayılan değeri null olsa da, girdi verisinde ona izin verilmez (değer
bir dize olmalıdır). null kabul eden özellikler nullable() kullanılarak tanımlanır:
$schema = Expect::structure([
'optional' => Expect::string(),
'nullable' => Expect::string()->nullable(),
]);
$processor->process($schema, ['optional' => null]);
// HATA: 'optional' bir dize olmasını bekler, null verildi.
$processor->process($schema, ['nullable' => null]);
// OK, {'optional' => null, 'nullable' => null} döndürür
Yapının tüm özelliklerinden oluşan diziyi getShape() metodu döndürür.
Varsayılan olarak girdi verisinde ek öğeler bulunamaz:
$schema = Expect::structure([
'key' => Expect::string(),
]);
$processor->process($schema, ['additional' => 1]);
// HATA: Beklenmeyen 'additional' öğesi
Bu, otherItems() kullanılarak değiştirilebilir. Parametre olarak, her fazladan öğeyi doğrulayacak şemayı
aktarın:
$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ıyı genişleterek yeni bir yapı oluşturabilirsiniz:
$dog = Expect::structure([
'name' => Expect::string(),
'age' => Expect::int(),
]);
$dogWithBreed = $dog->extend([
'breed' => Expect::string(),
]);
Dizi
Tanımlı anahtarları olan bir 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
]);
Tuple denen indeksli bir dizi de tanımlayabilirsiniz:
$schema = Expect::array([
Expect::int(),
Expect::string(),
Expect::bool(),
]);
$processor->process($schema, [1, 'hello', true]); // OK
Deprecated Özellikler
Bir özelliği deprecated([string $message]) metoduyla deprecated olarak işaretleyebilirsiniz. Deprecation
hakkındaki bilgi $processor->getWarnings() ile döndürülür:
$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"]
Aralıklar: min() max()
Diziler için öğe sayısını sınırlamak üzere min() ve max() kullanın:
// dizi, en az 10 öğe, en çok 20 öğe
Expect::array()->min(10)->max(20);
Dizeler için uzunluğunu sınırlar:
// dize, en az 10 karakter uzunluğunda, en çok 20 karakter
Expect::string()->min(10)->max(20);
Sayılar için değerini sınırlar:
// tam sayı, 10 ile 20 arasında, sınırlar dahil
Expect::int()->min(10)->max(20);
Elbette yalnızca min() ya da yalnızca max() belirtmek mümkündür:
// dize, en çok 20 karakter
Expect::string()->max(20);
Düzenli İfadeler: pattern()
pattern() kullanarak, girdi dizesinin tamamının eşleşmesi gereken bir düzenli ifade belirtebilirsiniz
(yani sanki ^ ve $ karakterleriyle sarılmış gibi):
// tam olarak 9 rakam
Expect::string()->pattern('\d{9}');
Özel Doğrulamalar: assert()
Başka her türlü kısıtlamayı assert(callable $fn) kullanarak ekleyebilirsiniz.
$countIsEven = fn($v) => count($v) % 2 === 0;
$schema = Expect::arrayOf('string')
->assert($countIsEven); // öğe sayısı çift olmalı
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // HATA: 3 çift bir sayı değil
Ya da
Expect::string()->assert('is_file'); // dosya var olmalı
Her doğrulamaya özel bir açıklama ekleyebilirsiniz. O, hata mesajının parçası olur.
$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.
Metot, birden çok kısıtlama eklemek için yinelemeli çağrılabilir. transform() ve castTo()
çağrılarıyla iç içe geçirilebilir.
Dönüşüm: transform()
Başarıyla doğrulanan veri, özel bir fonksiyonla değiştirilebilir:
// büyük harfe dönüştür:
Expect::string()->transform(fn(string $s) => strtoupper($s));
Metot, birden çok dönüşüm eklemek için yinelemeli çağrılabilir. assert() ve castTo()
çağrılarıyla iç içe geçirilebilir. İşlemler, bildirildikleri sırayla yapılır:
Expect::type('string|int')
->castTo('string')
->assert('ctype_lower', 'All characters must be lowercased')
->transform(fn(string $s) => strtoupper($s)); // büyük harfe dönüştür
transform() metodu değeri aynı anda hem dönüştürebilir hem doğrulayabilir. Bu sıklıkla
transform() ve assert() zincirlemekten daha basittir ve daha az kod yinelemesi içerir. Bu amaçla
fonksiyon, doğrulama sorunları hakkında bilgi eklemek için kullanılabilen addError() metoduna sahip bir Context nesnesi alır:
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);
});
Dönüştürme: castTo()
Başarıyla doğrulanan veri dönüştürülebilir:
Expect::scalar()->castTo('string');
Yerel PHP tiplerinin yanı sıra sınıflara da dönüştürebilirsiniz. Yapıcısı olmayan basit bir sınıf ile yapıcısı olan bir sınıf arasında ayrım yapar. Sınıfın yapıcısı yoksa bir örnek oluşturulur ve tüm yapı öğ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 ile $obj->refundAmount alanlarına yazar
Sınıfın yapıcısı varsa, yapı öğeleri yapıcıya adlandırılmış argümanlar olarak aktarılır:
class Info
{
public function __construct(
public bool $processRefund,
public int $refundAmount,
) {
}
}
// $obj = new Info(processRefund: ..., refundAmount: ...) oluşturur
Skaler bir parametreyle birleştirilen dönüştürme, bir nesne oluşturur ve değeri yapıcıya tek argüman olarak aktarır:
Expect::string()->castTo(DateTime::class);
// new DateTime(...) oluşturur
Normalleştirme: before()
Doğrulamanın kendisinden önce veri, before() metoduyla normalleştirilebilir. Örnek olarak, bir dize dizisi
olması gereken (örneğin ['a', 'b', 'c']), ama girdiyi a b c dizesi biçiminde 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 ürettirebilirsiniz. Örnek:
class Config
{
public string $name;
public string|null $password = null;
public bool $admin = false;
}
$schema = Expect::from(new Config);
$data = [
'name' => 'Frank',
];
$normalized = $processor->process($schema, $data);
// $normalized instanceof Config
// $normalized = {'name' => 'Frank', 'password' => null, 'admin' => false}
Anonim sınıflar da desteklenir:
$schema = Expect::from(new class {
public string $name;
public ?string $password = null;
public bool $admin = false;
});
Sınıf tanımından elde edilen bilgi yeterli olmayabileceğinden, öğeleri ikinci parametre kullanarak kendi şemanızla tamamlayabilirsiniz:
$schema = Expect::from(new Config, [
'name' => Expect::string()->pattern('\w:.*'),
]);
Birden Çok Yapılandırmayı Birleştirme
Uygulamalar yapılandırmalarını sıklıkla katmanlar hâlinde kurar: yerleşik varsayılan değerler vardır ve onların
üstüne kullanıcı, yalnızca gerçekten belirttiği öğeleri geçersiz kılması gereken kendi ayarlarını sağlar.
processMultiple() tam da bunu yapar: birkaç veri kümesini alır, sonrakiler öncelikli olacak biçimde onları
sırayla birleştirir ve nihai sonucu bir bütün olarak doğrular:
$schema = Expect::structure([
'host' => Expect::string(),
'port' => Expect::int(),
'logging' => Expect::bool(),
]);
$defaults = ['host' => 'localhost', 'port' => 3306, 'logging' => false];
$userConfig = ['port' => 5432, 'logging' => true];
$config = $processor->processMultiple($schema, [$defaults, $userConfig]);
// $config = {'host' => 'localhost', 'port' => 5432, 'logging' => true}
host öğesi, kullanıcı onu ayarlamadığından varsayılan değerini korur; port ve
logging ise sonraki veri kümesince üzerine yazılır. Dize anahtarlar altında saklanan değerler bu şekilde
birleştirilir; sayısal indeksli öğeler (listeler) ise üzerine yazılmak yerine art arda eklenir.
Kaputun Altında: normalize, merge, complete
Her şema öğesi, ister yerleşik olsun ister kendi yazdığınız, veriyi nasıl ele aldığını birlikte tanımlayan dört metodu gerçekleştirir. Onlardan üçü işleme hattını oluşturur:
- normalize() – ham girdiyi hazırlar.
before()kancaları burada çalışır ve örneğin bir nesne burada diziye dönüştürülür. İlk olarak, her veri kümesi üzerinde ayrı ayrı çalışır. - merge() – normalleştirilmiş iki veri kümesini, sonraki öncelikli olacak biçimde birleştirir. Bu adımı
yalnızca
processMultiple()kullanır;process()onu atlar, çünkü elinde tek bir veri kümesi vardır. - complete() – asıl doğrulamayı yapar, eksik öğeler için varsayılan değerleri doldurur ve
assert(),transform()ilecastTo()uygular. Birleştirilmiş sonuç üzerinde en son çalışır.
Dördüncü metot completeDefault(), girdide tümüyle eksik olan bir öğe için ana öğe tarafından çağrılır; ya
varsayılan değeri sağlar ya da bir required() öğesinin eksik olduğunu bildirir.
Yani process(), normalize → complete çalıştırır; processMultiple() ise normalize
(her veri kümesi) → merge → complete çalıştırır. before() metodunun ham girdiyi,
transform() metodunun ise zaten doğrulanmış değeri görmesinin nedeni bu sıradır.
Özel Şema Öğeleri
assert(), transform() ve before() ile epey yol alabilirsiniz, dolayısıyla sıfırdan
bir şey kurmanız nadiren gerekir. Ama kendi doğrulama ve birleştirme mantığı olan, yeniden kullanılabilir, kendi kendine
yeten bir öğe istediğinizde, Nette\Schema\Schema
arayüzünü gerçekleştirerek bir tane oluşturabilirsiniz. Onda tam olarak yukarıda anlatılan dört metot bulunur:
interface Schema
{
function normalize(mixed $value, Context $context);
function merge(mixed $value, mixed $base);
function complete(mixed $value, Context $context);
function completeDefault(Context $context);
}
Hatalar fırlatılmaz; onun yerine onları Context
nesnesi aracılığıyla $context->addError() ile bildirir ve null döndürürsünüz.
Processor tüm hataları toplar ve onları en sonda birlikte fırlatır.
Örnek olarak, bir enum'un backing değerini (örneğin 'hearts' dizesini) kabul eden ve enum örneğini
döndüren, yeniden kullanılabilir bir öğe kuralım:
use Nette\Schema\Context;
use Nette\Schema\Schema;
class EnumSchema implements Schema
{
public function __construct(
private string $enumClass,
) {
}
public function normalize(mixed $value, Context $context): mixed
{
return $value; // ön işleme gerekmiyor
}
public function merge(mixed $value, mixed $base): mixed
{
return $value ?? $base; // sonraki değer kazanır
}
public function complete(mixed $value, Context $context): mixed
{
$enum = is_string($value) ? ($this->enumClass)::tryFrom($value) : null;
if ($enum === null) {
$context->addError('The item %path% is not a valid value.', 'enum.value');
return null;
}
return $enum;
}
public function completeDefault(Context $context): mixed
{
return null; // öğe girdide eksikken kullanılan değer
}
}
Onu, yerleşik bir öğenin beklendiği her yerde kullanabilirsiniz: tek başına ya da daha büyük bir yapının parçası olarak:
enum Suit: string
{
case Hearts = 'hearts';
case Spades = 'spades';
}
$schema = Expect::structure([
'suit' => new EnumSchema(Suit::class),
]);
$processor->process($schema, ['suit' => 'hearts']);
// OK, {'suit' => Suit::Hearts} döndürür
Öğe arayüzün tamamını gerçekleştirdiğinden, processMultiple() içinde de otomatik çalışır;
Processor onun merge() metodunu tıpkı başka her öğede olduğu gibi çağırır.