Presenter'larda Formlar

Nette Forms, web formlarının oluşturulmasını ve işlenmesini önemli ölçüde kolaylaştırır. Bu bölümde, presenter'lar içinde formların kullanımını öğreneceksiniz.

Framework'ün geri kalanı olmadan tamamen bağımsız olarak nasıl kullanılacağını merak ediyorsanız, sizin için bağımsız kullanım kılavuzu bulunmaktadır.

İlk Form

Basit bir kayıt formu yazmayı deneyelim. Kodu şöyle olacaktır:

use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'İsim:');
$form->addPassword('password', 'Şifre:');
$form->addSubmit('send', 'Kaydol');
$form->onSuccess[] = [$this, 'formSucceeded'];

ve tarayıcıda şöyle görünecektir:

Presenter'daki form, Nette\Application\UI\Form sınıfının bir nesnesidir, öncülü Nette\Forms\Form bağımsız kullanım için tasarlanmıştır. Ona isim, şifre ve gönderme düğmesi olarak adlandırılan elemanları ekledik. Ve son olarak, $form->onSuccess satırı, gönderildikten ve başarılı bir şekilde doğrulandıktan sonra $this->formSucceeded() metodunun çağrılması gerektiğini söyler.

Presenter açısından form, sıradan bir bileşendir. Bu nedenle, bir bileşen olarak ele alınır ve fabrika metotları kullanılarak presenter'a dahil edilir. Şöyle görünecektir:

use Nette;
use Nette\Application\UI\Form;

class HomePresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentRegistrationForm(): Form
	{
		$form = new Form;
		$form->addText('name', 'İsim:');
		$form->addPassword('password', 'Şifre:');
		$form->addSubmit('send', 'Kaydol');
		$form->onSuccess[] = [$this, 'formSucceeded'];
		return $form;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// burada form tarafından gönderilen verileri işleyeceğiz
		// $data->name ismi içerir
		// $data->password şifreyi içerir
		$this->flashMessage('Başarıyla kaydoldunuz.');
		$this->redirect('Home:');
	}
}

Ve şablonda formu {control} etiketiyle render ederiz:

<h1>Kayıt</h1>

{control registrationForm}

Ve aslında hepsi bu :-) Çalışan ve mükemmel güvenli bir formumuz var.

Ve şimdi muhtemelen bunun çok hızlı olduğunu düşünüyorsunuz, formSucceeded() metodunun nasıl çağrıldığını ve aldığı parametrelerin ne olduğunu merak ediyorsunuz. Evet, haklısınız, bu açıklama gerektiriyor.

Nette, Hollywood tarzı dediğimiz taze bir mekanizma ile birlikte gelir. Bir geliştirici olarak sürekli bir şeylerin olup olmadığını sormak yerine (“form gönderildi mi?”, “geçerli bir şekilde gönderildi mi?” ve “sahtesi yapılmadı mı?”), framework'e “form geçerli bir şekilde doldurulduğunda, bu metodu çağır” dersiniz ve geri kalan işi ona bırakırsınız. JavaScript'te programlama yapıyorsanız, bu programlama tarzını yakından tanırsınız. Belirli bir olay gerçekleştiğinde çağrılan fonksiyonlar yazarsınız. Ve dil onlara ilgili argümanları iletir.

Yukarıdaki presenter kodu tam olarak bu şekilde oluşturulmuştur. $form->onSuccess dizisi, form gönderildiğinde ve doğru bir şekilde doldurulduğunda (yani geçerli olduğunda) Nette'nin çağıracağı PHP geri aramalarının (callback) bir listesini temsil eder. Presenter yaşam döngüsü çerçevesinde, bu sözde bir sinyaldir, yani action* metodundan sonra ve render* metodundan önce çağrılırlar. Ve her geri aramaya ilk parametre olarak formun kendisini ve ikinci parametre olarak gönderilen verileri ArrayHash nesnesi şeklinde iletir. Form nesnesine ihtiyacınız yoksa ilk parametreyi atlayabilirsiniz. Ve ikinci parametre daha akıllı olabilir, ancak bunun hakkında daha sonra konuşacağız.

$data nesnesi, kullanıcının doldurduğu verilerle name ve password anahtarlarını içerir. Genellikle verileri doğrudan daha fazla işleme göndeririz, bu örneğin veritabanına ekleme olabilir. Ancak işleme sırasında bir hata oluşabilir, örneğin kullanıcı adı zaten alınmış olabilir. Bu durumda, hatayı addError() kullanarak forma geri iletiriz ve hata mesajıyla birlikte yeniden render edilmesini sağlarız.

$form->addError('Üzgünüz, bu kullanıcı adı zaten kullanılıyor.');

onSuccess dışında bir de onSubmit vardır: geri aramalar, form doğru doldurulmamış olsa bile her zaman form gönderildikten sonra çağrılır. Ve ayrıca onError: geri aramalar yalnızca gönderim geçerli değilse çağrılır. onSuccess veya onSubmit içinde formu addError() ile geçersiz kılsak bile çağrılırlar.

Formu işledikten sonra bir sonraki sayfaya yönlendiririz. Bu, yenile, geri düğmesiyle veya tarayıcı geçmişinde gezinerek formun istenmeyen şekilde yeniden gönderilmesini önler.

Diğer form elemanları eklemeyi deneyin.

Elemanlara Erişim

Form, presenter'ın bir bileşenidir, bizim durumumuzda registrationForm olarak adlandırılmıştır (fabrika metodu createComponentRegistrationForm adına göre), bu nedenle presenter'ın herhangi bir yerinde forma şu şekilde erişebilirsiniz:

$form = $this->getComponent('registrationForm');
// alternatif sözdizimi: $form = $this['registrationForm'];

Bireysel form elemanları da bileşenlerdir, bu nedenle onlara aynı şekilde erişebilirsiniz:

$input = $form->getComponent('name'); // veya $input = $form['name'];
$button = $form->getComponent('send'); // veya $button = $form['send'];

Elemanlar unset ile kaldırılır:

unset($form['name']);

Doğrulama Kuralları

Geçerli kelimesi geçti, ancak formun henüz herhangi bir doğrulama kuralı yok. Bunu düzeltelim.

İsim zorunlu olacak, bu yüzden onu setRequired() metoduyla işaretleyeceğiz, argümanı kullanıcı ismi doldurmazsa görüntülenecek hata mesajının metnidir. Argüman belirtmezsek, varsayılan hata mesajı kullanılır.

$form->addText('name', 'İsim:')
	->setRequired('Lütfen ismi girin');

Formu doldurulmuş isim olmadan göndermeyi deneyin ve bir hata mesajının görüntülendiğini ve tarayıcının veya sunucunun alanı doldurana kadar reddedeceğini göreceksiniz.

Aynı zamanda, sisteme sadece boşluk yazarak hile yapamazsınız. Hayır. Nette sol ve sağ boşlukları otomatik olarak kaldırır. Deneyin. Bu, her tek satırlık girişle her zaman yapmanız gereken bir şeydir, ancak genellikle unutulur. Nette bunu otomatik olarak yapar. (Formu aldatmayı deneyebilir ve isim olarak çok satırlı bir dize gönderebilirsiniz. Nette burada da aldanmaz ve satır sonlarını boşluklara dönüştürür.)

Form her zaman sunucu tarafında doğrulanır, ancak aynı zamanda anında gerçekleşen ve kullanıcının hatayı formu sunucuya göndermeye gerek kalmadan hemen öğrendiği JavaScript doğrulaması da üretilir. Bu, netteForms.js betiği tarafından yapılır. Bunu layout şablonuna ekleyin:

<script src="http://unpkg.com/nette-forms@3"></script>

Form içeren sayfanın kaynak koduna bakarsanız, Nette'nin zorunlu elemanları required CSS sınıfına sahip elemanlara eklediğini fark edebilirsiniz. Şablona aşağıdaki stil sayfasını eklemeyi deneyin ve “İsim” etiketi kırmızı olacaktır. Bu şekilde kullanıcılara zorunlu elemanları zarifçe işaretleriz:

<style>
.required label { color: maroon }
</style>

Diğer doğrulama kurallarını addRule() metoduyla ekleriz. İlk parametre kuraldır, ikincisi yine hata mesajının metnidir ve bunu doğrulama kuralının bir argümanı takip edebilir. Bununla ne kastediliyor?

Formu, tamsayı olması gereken (addInteger()) ve ayrıca izin verilen bir aralıkta ($form::Range) olması gereken yeni isteğe bağlı “yaş” alanıyla genişleteceğiz. Ve burada tam olarak addRule() metodunun üçüncü parametresini kullanacağız, bununla doğrulayıcıya istenen aralığı [başlangıç, bitiş] çifti olarak ileteceğiz:

$form->addInteger('age', 'Yaş:')
	->addRule($form::Range, 'Yaş 18 ile 120 arasında olmalıdır', [18, 120]);

Kullanıcı alanı doldurmazsa, eleman isteğe bağlı olduğu için doğrulama kuralları kontrol edilmeyecektir.

Burada küçük bir yeniden düzenleme için yer var. Hata mesajında ve üçüncü parametrede sayılar yinelenmiştir, bu ideal değildir. Eğer çok dilli formlar oluşturuyor olsaydık ve sayıları içeren mesaj birden çok dile çevrilmiş olsaydı, değerlerin olası bir değişikliği zorlaşırdı. Bu nedenle, %d yer tutucularını kullanmak mümkündür ve Nette değerleri tamamlayacaktır:

	->addRule($form::Range, 'Yaş %d ile %d arasında olmalıdır', [18, 120]);

Aynı zamanda zorunlu hale getireceğimiz ve ayrıca şifrenin minimum uzunluğunu ($form::MinLength) doğrulayacağımız password elemanına geri dönelim, yine yer tutucu kullanarak:

$form->addPassword('password', 'Şifre:')
	->setRequired('Bir şifre seçin')
	->addRule($form::MinLength, 'Şifre en az %d karakter olmalıdır', 8);

Forma bir de passwordVerify alanı ekleyelim, burada kullanıcı kontrol için şifreyi tekrar girecektir. Doğrulama kurallarını kullanarak her iki şifrenin aynı olup olmadığını kontrol edeceğiz ($form::Equal). Ve parametre olarak ilk şifreye köşeli parantezler kullanarak bir referans vereceğiz:

$form->addPassword('passwordVerify', 'Kontrol için şifre:')
	->setRequired('Lütfen kontrol için şifreyi tekrar girin')
	->addRule($form::Equal, 'Şifreler eşleşmiyor', $form['password'])
	->setOmitted();

setOmitted() kullanarak, değerinin aslında bizim için önemli olmadığı ve yalnızca doğrulama amacıyla var olan elemanı işaretledik. Değer $data'ya iletilmez.

Böylece PHP ve JavaScript'te doğrulaması olan tamamen işlevsel bir formumuz oldu. Nette'nin doğrulama yetenekleri çok daha geniştir, koşullar oluşturabilir, bunlara göre sayfanın bölümlerini gösterebilir ve gizleyebilirsiniz vb. Her şeyi form doğrulaması bölümünde öğreneceksiniz.

Varsayılan Değerler

Form elemanlarına genellikle varsayılan değerler atarız:

$form->addEmail('email', 'E-posta')
	->setDefaultValue($lastUsedEmail);

Genellikle tüm elemanlara aynı anda varsayılan değerler atamak kullanışlıdır. Örneğin, form kayıtları düzenlemek için kullanıldığında. Veritabanından kaydı okur ve varsayılan değerleri atarız:

//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);

setDefaults()'u elemanları tanımladıktan sonra çağırın.

Formun Render Edilmesi

Standart olarak form bir tablo olarak render edilir. Bireysel elemanlar temel erişilebilirlik kuralını karşılar – tüm etiketler <label> olarak yazılır ve ilgili form elemanıyla ilişkilendirilir. Etikete tıklandığında imleç otomatik olarak form alanında görünür.

Her elemana istediğimiz HTML niteliklerini atayabiliriz. Örneğin bir yer tutucu ekleyebiliriz:

$form->addInteger('age', 'Yaş:')
	->setHtmlAttribute('placeholder', 'Lütfen yaşı girin');

Formu render etmenin gerçekten çok sayıda yolu vardır, bu nedenle buna ayrılmış renderleme hakkında ayrı bir bölüm bulunmaktadır.

Sınıflara Eşleme

İkinci parametre $data'da gönderilen verileri ArrayHash nesnesi olarak alan formSucceeded() metoduna geri dönelim. Bu, stdClass gibi genel bir sınıf olduğundan, onunla çalışırken belirli bir konfor eksikliği yaşayacağız, örneğin editörlerde özelliklerin önerilmesi veya statik kod analizi gibi. Bu, her form için özelliklerinin bireysel elemanları temsil ettiği belirli bir sınıfa sahip olarak çözülebilir. Örneğin:

class RegistrationFormData
{
	public string $name;
	public ?int $age;
	public string $password;
}

Alternatif olarak, yapıcıyı kullanabilirsiniz:

class RegistrationFormData
{
	public function __construct(
		public string $name,
		public ?int $age,
		public string $password,
	) {
	}
}

Veri sınıfının özellikleri enum'lar da olabilir ve otomatik olarak eşlenirler.

Nette'ye verileri bu sınıfın nesneleri olarak döndürmesini nasıl söyleriz? Düşündüğünüzden daha kolay. Sınıfı işleyici metodundaki $data parametresinin türü olarak belirtmek yeterlidir:

public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $data, RegistrationFormData örneğidir
	$name = $data->name;
	// ...
}

Tür olarak array de belirtebilirsiniz ve o zaman verileri bir dizi olarak iletir.

Benzer şekilde, sınıf adını veya hidratlanacak nesneyi parametre olarak ilettiğimiz getValues() fonksiyonunu da kullanabilirsiniz:

$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;

Formlar konteynerlerden oluşan çok seviyeli bir yapı oluşturuyorsa, her biri için ayrı bir sınıf oluşturun:

$form = new Form;
$person = $form->addContainer('person');
$person->addText('firstName');
/* ... */

class PersonFormData
{
	public string $firstName;
	public string $lastName;
}

class RegistrationFormData
{
	public PersonFormData $person;
	public ?int $age;
	public string $password;
}

Eşleme daha sonra $person özelliğinin türünden, konteyneri PersonFormData sınıfına eşlemesi gerektiğini anlar. Eğer özellik bir konteyner dizisi içeriyorsa, array türünü belirtin ve eşleme için sınıfı doğrudan konteynere iletin:

$person->setMappedType(PersonFormData::class);

Formun veri sınıfının tasarımını Nette\Forms\Blueprint::dataClass($form) metodunu kullanarak oluşturabilirsiniz, bu da onu tarayıcı sayfasına yazdırır. Kodu daha sonra tıklayarak işaretleyip projeye kopyalamak yeterlidir.

Birden Fazla Düğme

Formun birden fazla düğmesi varsa, genellikle hangisine basıldığını ayırt etmemiz gerekir. Her düğme için kendi işleyici fonksiyonumuzu oluşturabiliriz. Bunu olay onClick için bir işleyici olarak ayarlayacağız:

$form->addSubmit('save', 'Kaydet')
	->onClick[] = [$this, 'saveButtonPressed'];

$form->addSubmit('delete', 'Sil')
	->onClick[] = [$this, 'deleteButtonPressed'];

Bu işleyiciler, tıpkı onSuccess olayında olduğu gibi, yalnızca geçerli bir şekilde doldurulmuş form durumunda çağrılır. Fark, ilk parametre olarak form yerine gönderme düğmesinin iletilebilmesidir, belirttiğiniz türe bağlıdır:

public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
	$form = $button->getForm();
	// ...
}

Form Enter tuşuyla gönderildiğinde, ilk düğmeyle gönderilmiş gibi kabul edilir.

onAnchor Olayı

Fabrika metodunda (örneğin createComponentRegistrationForm gibi) formu oluştururken, form henüz gönderilip gönderilmediğini veya hangi verilerle gönderildiğini bilmez. Ancak gönderilen değerleri bilmemiz gereken durumlar vardır, örneğin formun sonraki şekli bunlara bağlıdır veya bağımlı seçme kutuları (select box) için onlara ihtiyacımız vardır vb.

Bu nedenle, formu oluşturan kodun bir kısmını, yalnızca sözde demirlendiğinde, yani presenter ile zaten bağlantılı olduğunda ve gönderilen verilerini bildiğinde çağrılmasını sağlayabilirsiniz. Böyle bir kodu $onAnchor dizisine iletiriz:

$country = $form->addSelect('country', 'Ülke:', $this->model->getCountries());
$city = $form->addSelect('city', 'Şehir:');

$form->onAnchor[] = function () use ($country, $city) {
	// bu fonksiyon, formun gönderilip gönderilmediğini ve hangi verilerle gönderildiğini bildiğinde çağrılır
	// bu nedenle getValue() metodu kullanılabilir
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};

Güvenlik Açıklarına Karşı Koruma

Nette Framework güvenliğe büyük önem verir ve bu nedenle formların iyi bir şekilde korunmasına özen gösterir. Bunu tamamen şeffaf bir şekilde yapar ve manuel olarak hiçbir şey ayarlamayı gerektirmez.

Formları Cross Site Scripting (XSS) ve Cross-Site Request Forgery (CSRF) saldırılarına karşı korumanın yanı sıra, sizin artık düşünmeniz gerekmeyen birçok küçük güvenlik önlemi alır.

Örneğin, girdilerden tüm kontrol karakterlerini filtreler ve UTF-8 kodlamasının geçerliliğini kontrol eder, böylece formdan gelen veriler her zaman temiz olur. Seçme kutuları (select box) ve radyo listelerinde, seçilen öğelerin gerçekten sunulanlardan olduğunu ve sahtesinin yapılmadığını doğrular. Tek satırlık metin girişlerinde, saldırganın oraya göndermiş olabileceği satır sonu karakterlerini kaldırdığını zaten belirtmiştik. Çok satırlık girişlerde ise satır sonu karakterlerini normalleştirir. Ve böyle devam eder.

Nette, birçok programcının varlığından bile haberdar olmadığı güvenlik risklerini sizin için çözer.

Bahsedilen CSRF saldırısı, bir saldırganın kurbanı, kurbanın oturum açtığı sunucuya kurbanın tarayıcısında fark ettirmeden bir istek gerçekleştiren bir sayfaya çekmesinden oluşur ve sunucu, isteğin kurban tarafından kendi isteğiyle gerçekleştirildiğini varsayar. Bu nedenle Nette, POST formunun başka bir alan adından gönderilmesini engeller. Herhangi bir nedenle korumayı kapatmak ve formun başka bir alan adından gönderilmesine izin vermek isterseniz, şunu kullanın:

$form->allowCrossOrigin(); // DİKKAT! Korumayı kapatır!

Bu koruma, _nss adlı SameSite çerezini kullanır. SameSite çereziyle koruma %100 güvenilir olmayabilir, bu nedenle token ile korumayı da etkinleştirmek önerilir:

$form->addProtection();

Uygulamadaki hassas verileri değiştiren sitenin yönetim bölümündeki formları bu şekilde korumanızı öneririz. Framework, oturumda saklanan bir yetkilendirme token'ı üreterek ve doğrulayarak CSRF saldırısına karşı kendini savunur. Bu nedenle, formu görüntülemeden önce oturumun açık olması gerekir. Sitenin yönetim bölümünde, genellikle kullanıcı girişi nedeniyle oturum zaten başlatılmıştır. Aksi takdirde, oturumu Nette\Http\Session::start() metoduyla başlatın.

Birden Fazla Presenter'da Aynı Form

Bir formu birden fazla presenter'da kullanmanız gerekiyorsa, bunun için bir fabrika oluşturmanızı ve ardından bunu presenter'a iletmenizi öneririz. Böyle bir sınıf için uygun bir konum, örneğin app/Forms dizinidir.

Fabrika sınıfı şöyle görünebilir:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'İsim:');
		$form->addSubmit('send', 'Giriş yap');
		return $form;
	}
}

Sınıftan, presenter'daki bileşenler için fabrika metodunda formu üretmesini isteriz:

public function __construct(
	private SignInFormFactory $formFactory,
) {
}

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// formu değiştirebiliriz, burada örneğin düğme üzerindeki etiketi değiştiriyoruz
	$form['send']->setCaption('Devam et');
	$form->onSuccess[] = [$this, 'signInFormSuceeded']; // ve bir işleyici ekliyoruz
	return $form;
}

Form işleme için işleyici, fabrikadan da sağlanabilir:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'İsim:');
		$form->addSubmit('send', 'Giriş yap');
		$form->onSuccess[] = function (Form $form, $data): void {
			// burada form işlemeyi gerçekleştiriyoruz
		};
		return $form;
	}
}

İşte, Nette'deki formlara hızlı bir giriş yaptık. Dağıtımdaki examples dizinine göz atmayı deneyin, burada daha fazla ilham bulacaksınız.

versiyon: 4.0