Presenter'larda Formlar
Nette Forms, web formlarının oluşturulmasını ve işlenmesini belirgin biçimde kolaylaştırır. Bu bölümde formları presenter'ların içinde nasıl kullanacağınızı öğreneceksiniz.
Onları framework'ün geri kalanı olmadan tümüyle bağımsız kullanmak istiyorsanız, bağımsız kullanım için bir kılavuz var.
İlk Form
Basit bir kayıt formu yazmayı deneyelim. Kodu şöyle olacak:
use Nette\Application\UI\Form;
$form = new Form;
$form->addText('name', 'Ad:');
$form->addPassword('password', 'Parola:');
$form->addSubmit('send', 'Kaydol');
$form->onSuccess[] = $this->formSucceeded(...);
ve tarayıcıda şöyle görünecek:

Presenter'daki bir form, Nette\Application\UI\Form sınıfının bir nesnesidir; öncülü
Nette\Forms\Form ise bağımsız kullanım içindir. name, password adlı öğeleri ve bir gönder düğmesi ekledik.
Son olarak $form->onSuccess satırı, gönderimden ve başarılı doğrulamadan sonra
$this->formSucceeded() metodunun çağrılacağını belirtir.
Presenter'ın bakış açısından form sıradan bir bileşendir. Bu yüzden bileşen olarak ele alınır ve presenter'a bir factory metoduyla katılır. Şöyle görünecek:
use Nette;
use Nette\Application\UI\Form;
class HomePresenter extends Nette\Application\UI\Presenter
{
protected function createComponentRegistrationForm(): Form
{
$form = new Form;
$form->addText('name', 'Ad:');
$form->addPassword('password', 'Parola:');
$form->addSubmit('send', 'Kaydol');
$form->onSuccess[] = $this->formSucceeded(...);
return $form;
}
private function formSucceeded(Form $form, $data): void
{
// formun gönderdiği veriyi burada işleyeceğiz
// $data->name adı içerir
// $data->password parolayı içerir
$this->flashMessage('Başarıyla kaydoldunuz.');
$this->redirect('Home:');
}
}
Şablonda ise form {control} etiketiyle render edilir:
<h1>Kayıt</h1>
{control registrationForm}
Ve aslında hepsi bu :-) İşleyen ve kusursuz biçimde güvenli bir formumuz var.
Şimdi muhtemelen bunun çok hızlı olduğunu düşünüyor, formSucceeded() metodunun nasıl çağrıldığını
ve hangi parametreleri aldığını merak ediyorsunuz. Evet, haklısınız, bu bir açıklamayı hak ediyor.
Nette, Hollywood stili adlı ferahlatıcı bir düzenek getirir. Geliştirici olarak sizin sürekli bir şey olup olmadığını sormanız (“form gönderildi mi?”, “geçerli biçimde mi gönderildi?”, “sahte değil mi?”) yerine, framework'e “form geçerli biçimde doldurulduğunda şu metodu çağır” dersiniz ve sonraki işi ona bırakırsınız. JavaScript programlıyorsanız bu programlama stilini yakından biliyorsunuzdur. Belirli bir olay gerçekleştiğinde çağrılan fonksiyonlar yazarsınız. Ve dil onlara uygun argümanları aktarır.
Yukarıdaki presenter kodu tam olarak böyle kurulmuştur. $form->onSuccess dizisi, formun gönderildiği ve
doğru doldurulduğu (yani geçerli olduğu) anda Nette'in çağırdığı PHP callback'lerinin listesini temsil eder. Presenter yaşam döngüsünde bu bir
sinyaldir, dolayısıyla action* metodundan sonra ve render* metodundan önce çağrılırlar. Ve her
callback'e ilk parametre olarak formun kendisini, ikinci parametre olarak da gönderilen veriyi bir ArrayHash nesnesi (ya da stdClass veya özel bir sınıf) olarak
aktarır. Form nesnesine ihtiyacınız yoksa ilk parametreyi atlayabilirsiniz. İkinci parametre daha akıllı olabilir, ama bunun
ayrıntısı daha sonra.
$data nesnesi, kullanıcının girdiği verilerle birlikte name ve password
özelliklerini içerir. Genellikle veriyi doğrudan ileri 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. Böyle bir durumda hatayı
addError() ile forma geri aktarır 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 onSubmit de vardır: callback'leri, form doğru doldurulmamış olsa bile her
gönderildiğinde çağrılır. Bir de onError vardır: callback'leri yalnızca gönderim geçerli değilse
çağrılır. onSuccess içinde addError() ile formu geçersiz kılarsak bile çağrılırlar.
Formu işledikten sonra başka bir sayfaya yönlendiririz. Bu, yenile ya da geri düğmesi veya tarayıcı geçmişinde gezinme yoluyla formun istenmeden yeniden gönderilmesini önler.
Form AJAX ile gönderilirse, yönlendirme yerine genellikle yeniden render edilmiş formu içeren bir snippet yeniden çizersiniz.
Başka form öğeleri eklemeyi deneyin.
Öğelere Erişim
Form, presenter'ın bir bileşenidir; bizim örneğimizde registrationForm adını taşır (factory metodunun adı
createComponentRegistrationForm olduğundan), dolayısıyla presenter'ın herhangi bir yerinde forma şöyle
erişebilirsiniz:
$form = $this->getComponent('registrationForm');
// alternatif söz dizimi: $form = $this['registrationForm'];
Tek tek form öğeleri de bileşendir, bu yüzden onlara da aynı şekilde erişebilirsiniz:
$input = $form->getComponent('name'); // ya da $input = $form['name'];
$button = $form->getComponent('send'); // ya da $button = $form['send'];
Öğeler unset ile kaldırılır:
unset($form['name']);
Doğrulama Kuralları
Geçerli sözcüğü geçti, ama formun henüz hiçbir doğrulama kuralı yok. Bunu düzeltelim.
Ad zorunlu olacak, bu yüzden onu setRequired() metoduyla işaretliyoruz. Argümanı, kullanıcı adı doldurmazsa
görüntülenecek hata mesajının metnidir. Argüman atlanırsa varsayılan hata mesajı kullanılır.
$form->addText('name', 'Ad:')
->setRequired('Lütfen adınızı girin.');
Formu adı doldurmadan göndermeyi deneyin; bir hata mesajının göründüğünü ve tarayıcının ya da sunucunun siz alanı doldurana dek onu reddettiğini göreceksiniz.
Aynı zamanda, örneğin alana yalnızca boşluk girerek sistemi kandıramazsınız. Mümkün değil. Nette, baştaki ve sondaki boşlukları otomatik olarak kırpar. Deneyin. Bu, her tek satırlık girdide her zaman yapmanız gereken, ama sık sık unutulan bir şeydir. Nette onu otomatik yapar. (Formu kandırıp ad olarak çok satırlı bir dize göndermeyi deneyebilirsiniz. Burada da Nette kanmaz ve satır sonları boşluğa dönüştürülür.)
Form her zaman sunucu tarafında doğrulanır, ama anında çalışan bir JavaScript doğrulaması da üretilir; böylece
kullanıcı, formu sunucuya göndermeye gerek kalmadan hatayı hemen öğrenir. Bunu netteForms.js betiği üstlenir.
Onu yerleşim şablonunuza ekleyin:
<script src="https://unpkg.com/nette-forms@3"></script>
Formun bulunduğu sayfanın kaynak koduna bakarsanız, Nette'in zorunlu öğeleri required CSS sınıfına sahip
elemanlarla sardığını fark edebilirsiniz. Şablonunuza aşağıdaki stil sayfasını eklemeyi deneyin; “Ad” etiketi
kırmızı olacak. Bu, zorunlu alanları kullanıcılar için şık biçimde vurgular:
<style>
.required label { color: maroon }
</style>
Başka doğrulama kurallarını addRule() metoduyla ekleriz. İlk parametre kural, ikincisi yine hata mesajının
metni, ardından da doğrulama kuralı için bir argüman gelebilir. Bu ne demek?
Formu, tam sayı olması (addInteger()) ve ayrıca izin verilen bir aralıkta bulunması
($form::Range) gereken yeni ve isteğe bağlı bir “yaş” alanıyla genişletelim. Burada, gereken aralığı
doğrulayıcıya [min, max] çifti olarak aktarmak için addRule() metodunun üçüncü parametresini
kullanacağız:
$form->addInteger('age', 'Yaş:')
->addRule($form::Range, 'Yaş 18 ile 120 arasında olmalıdır.', [18, 120]);
Kullanıcı alanı doldurmazsa doğrulama kuralları denetlenmez, çünkü öğe isteğe bağlıdır.
Bu, küçük bir yeniden düzenlemeye yer açar. Hata mesajında ve üçüncü parametrede sayılar yineleniyor; bu ideal
değil. Çok dilli formlar yapıyor olsaydık ve sayı içeren
mesaj birden çok dile çevrilseydi, değerleri değiştirmek zorlaşırdı. Bu nedenle %d yer tutucuları
kullanılabilir ve Nette değerleri yerine koyar:
->addRule($form::Range, 'Yaş %d ile %d yaş arasında olmalıdır.', [18, 120]);
password öğesine dönelim, onu da zorunlu yapalım ve ayrıca en az parola uzunluğunu
($form::MinLength) doğrulayalım; yine mesajda bir yer tutucu kullanarak:
$form->addPassword('password', 'Parola:')
->setRequired('Bir parola seçin')
->addRule($form::MinLength, 'Parolanız en az %d karakter uzunluğunda olmalıdır.', 8);
Forma, kullanıcının parolayı doğrulama için yeniden girdiği passwordVerify adlı bir alan daha ekleyelim.
Doğrulama kurallarıyla iki parolanın aynı olup olmadığını denetliyoruz ($form::Equal). Argüman olarak ilk
parolaya köşeli parantezlerle bir referans veriyoruz:
$form->addPassword('passwordVerify', 'Parola tekrar:')
->setRequired('Yazım hatası olmadığını denetlemek için parolanızı yeniden girin')
->addRule($form::Equal, 'Parolalar eşleşmiyor.', $form['password'])
->setOmitted();
setOmitted() ile, değeri aslında bizi ilgilendirmeyen ve yalnızca doğrulama amacıyla var olan bir öğeyi
işaretledik. Değeri $data içine aktarılmaz.
Böylece hem PHP hem JavaScript doğrulamalı, tam işleyen bir formumuz oldu. Nette'in doğrulama yetenekleri çok daha geniştir; koşullar oluşturulabilir, onlara göre sayfanın parçaları gösterilip gizlenebilir vb. Her şeyi form doğrulama bölümünde öğreneceksiniz.
Varsayılan Değerler
Form öğeleri için genellikle varsayılan değerler ayarlarız:
$form->addEmail('email', 'E-posta')
->setDefaultValue($lastUsedEmail);
Tüm öğeler için varsayılan değerleri aynı anda ayarlamak çoğu zaman işe yarar. Örneğin form kayıt düzenlemek için kullanıldığında. Kaydı veritabanından okur ve varsayılan değerleri ayarlarız:
// $row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);
setDefaults() metodunu öğeleri tanımladıktan sonra çağırın.
Zaten gönderilmiş bir formda setDefaults() etkisizdir; kullanıcının doldurduğunun üzerine yazmaz, bu
yüzden onu form factory'sinde koşulsuz çağırmak güvenlidir. Değerleri gönderimden sonra da zorlamanız gerekiyorsa bunun
yerine setValues() kullanın.
Formun Render Edilmesi
Form varsayılan olarak bir tablo olarak render edilir. Tek tek öğeler temel web erişilebilirlik kurallarına uyar; tüm
etiketler <label> elemanı olarak yazılır ve ilgili form öğeleriyle ilişkilendirilir. Etikete tıklamak
imleci otomatik olarak form alanına odaklar.
Her öğe için istediğimiz HTML niteliklerini ayarlayabiliriz. Örneğin bir placeholder ekleyelim:
$form->addInteger('age', 'Yaş:')
->setHtmlAttribute('placeholder', 'Lütfen yaşı girin');
Bir formu render etmenin gerçekten pek çok yolu var, bu yüzden ona ayrı bir render bölümü ayrıldı.
Sınıflara Eşleme
formSucceeded() metoduna dönelim; ikinci $data parametresinde gönderilen veriyi bir
ArrayHash nesnesi (ya da stdClass) olarak alıyor. Bu, stdClass gibi genel bir sınıf
olduğundan, onunla çalışırken düzenleyicilerde özellik tamamlama ya da statik kod çözümlemesi gibi bazı kolaylıklardan
yoksun kalırız. Bu, her form için, özellikleri tek tek öğeleri temsil eden özel bir sınıf yazılarak çözülebilir.
Örneğin:
class RegistrationFormData
{
public string $name;
public ?int $age;
public string $password;
}
Alternatif olarak bir yapıcı kullanabilirsiniz:
class RegistrationFormData
{
public function __construct(
public string $name,
public ?int $age,
public string $password,
) {
}
}
Veri sınıfının özellikleri enum da olabilir ve otomatik olarak eşlenirler.
Nette'e veriyi bu sınıfın nesneleri olarak döndürmesini nasıl söyleriz? Sandığınızdan kolay. İşleyici metodunda
$data parametresinin türü olarak yalnızca sınıfı belirtin:
public function formSucceeded(Form $form, RegistrationFormData $data): void
{
// $data, RegistrationFormData örneğidir
$name = $data->name;
// ...
}
Tür olarak array de belirtebilirsiniz; o zaman veri dizi olarak aktarılır.
Benzer şekilde, parametre olarak sınıf adını ya da doldurulacak bir nesneyi vererek getValues() metodunu
kullanabilirsiniz:
$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;
Değerleri form doğrulanmadan önce okumanız gerekiyorsa (tipik olarak bir onValidate işleyicisinin içinde),
bunun yerine getUntrustedValues() metodunu kullanın. getValues() ile aynı parametreleri alır, ama
gönderilen değerleri doğrulamadan geçtiklerini güvence altına almadan döndürür.
Formlar container'lardan oluşan çok düzeyli bir yapıya sahipse, 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, container'ı PersonFormData sınıfına
eşlemesi gerektiğini çıkarır. Özellik container dizisi içerecekse array türünü belirtin ve eşlenecek
sınıfı doğrudan container'a verin:
$person->setMappedType(PersonFormData::class);
Formun veri sınıfı için bir taslağı, onu tarayıcı sayfasına yazdıran
Nette\Forms\Blueprint::dataClass($form) metoduyla üretebilirsiniz. Sonra yalnızca tıklayıp kodu seçin ve
projenize kopyalayın.
Birden Çok Gönder Düğmesi
Formun birden çok düğmesi varsa, genellikle hangisine basıldığını ayırt etmemiz gerekir. Her düğme için ayrı bir
işleyici fonksiyon yazabiliriz. Onu onClick olayının
işleyicisi olarak ayarlayın:
$form->addSubmit('save', 'Kaydet')
->onClick[] = $this->saveButtonPressed(...);
$form->addSubmit('delete', 'Sil')
->onClick[] = $this->deleteButtonPressed(...);
Bir işleyici, addSubmit() metodunun üçüncü argümanı olarak doğrudan düğmeye de
verilebilir.
Bu işleyiciler, tıpkı onSuccess olayı gibi, yalnızca form geçerli biçimde doldurulduğunda çağrılır
(düğme için doğrulama kapatılmadıysa). Fark, belirttiğiniz tür bildirimine göre ilk parametre olarak form yerine gönder
düğmesi nesnesinin aktarılabilmesidir:
private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
$form = $button->getForm();
// ...
}
Form Enter tuşuna basılarak gönderildiğinde, ilk gönder düğmesiyle gönderilmiş gibi ele alınır.
onAnchor Olayı
Bir formu factory metodunda kurduğunuzda (örneğin createComponentRegistrationForm), form henüz gönderilip
gönderilmediğini ya da hangi veriyle gönderildiğini bilmez. Ancak gönderilen değerleri bilmemiz gereken durumlar vardır;
belki formun görünümü onlara bağlıdır ya da birbirine bağlı seçim kutuları için gerekirler vb.
Bu yüzden formu kuran kodun yalnızca form “demirlendiğinde”, yani presenter'a bağlanıp gönderilen verisini
bildiğinde çağrılmasını sağlayabilirsiniz. Böyle bir kodu $onAnchor dizisine koyun:
$country = $form->addSelect('country', 'Ülke:', $this->model->getCountries());
$city = $form->addSelect('city', 'Şehir:');
$form->onAnchor[] = function () use ($country, $city) {
// bu fonksiyon, form hangi veriyle gönderildiğini bildiğinde çağrılır
// böylece getValue() metodunu kullanabilirsiniz
$val = $country->getValue();
$city->setItems($val ? $this->model->getCities($val) : []);
};
Açıklara Karşı Koruma
Nette Framework güvenliğe büyük önem verir ve bu yüzden formların güvenliğini titizlikle sağlar. Bunu tümüyle saydam biçimde yapar ve elle hiçbir ayar gerektirmez.
Formları Cross-Site Scripting (XSS) ve Cross-Site Request Forgery (CSRF) gibi saldırılara karşı korumanın yanı sıra, artık düşünmenize gerek kalmayan pek çok küçük güvenlik önlemi de alır.
Örneğin girdilerdeki tüm denetim karakterlerini süzer ve UTF-8 kodlamasının geçerliliğini denetler; böylece formdan gelen verinin her zaman temiz olmasını sağlar. Seçim kutuları ve radyo listelerinde, seçilen öğelerin gerçekten sunulanlar arasında olduğunu ve hiçbir sahtecilik yapılmadığını doğrular. Tek satırlık metin girdilerinde, bir saldırganın gönderebileceği satır sonu karakterlerini boşlukla değiştirdiğini zaten söylemiştik. Çok satırlı girdilerde satır sonu karakterlerini normalleştirir. Ve böyle sürer.
Nette, pek çok programcının var olduğunu bile bilmediği güvenlik risklerini sizin yerinize halleder.
Sözü edilen CSRF saldırısı, bir saldırganın kurbanı, kurbanın tarayıcısında sessizce, kurbanın oturum açmış olduğu sunucuya bir istek çalıştıran bir sayfaya çekmesinden ibarettir. Sunucu da isteğin kurban tarafından isteyerek yapıldığına inanır. Bu yüzden Nette, yabancı bir kaynaktan gönderilen POST formlarını reddeder; aynı sitenin farklı bir alt alan adı bile yabancı sayılır. Başka bir kaynaktan gönderime izin vermeniz gerekiyorsa korumayı şununla kapatın:
$form->allowCrossOrigin(); // UYARI! Korumayı tümüyle kapatır!
Ancak bu, korumayı her kaynak için kapatır. Yalnızca belirli kaynaklara izin vermek için korumayı kapatın ve
Origin header'ını kendi izin listenize göre kendiniz doğrulayın.
Koruma, tarayıcının otomatik gönderdiği ve bir XSS açığıyla bile taklit edilemeyen Sec-Fetch-Site
header'ına (Fetch Metadata) dayanır. Bunları desteklemeyen eski tarayıcılarda, Nette uygulamasının otomatik ayarladığı
bir SameSite çerezi yedek olarak devreye girer. Tarayıcı sonunda
CSRF'yi çözüyor yazısı bunu ayrıntılı anlatıyor.
Oturumda saklanan bir yetkilendirme token'ıyla yapılan ve $form->addProtection() ile
etkinleştirilen önceki koruma artık gerekmiyor ve 3.3 sürümünden beri kullanımdan kaldırıldı.
Aynı Formu Birden Çok Presenter'da Kullanma
Aynı formu birden çok presenter'da kullanmanız gerekiyorsa, onun için bir factory oluşturup presenter'lara enjekte
etmenizi öneririz. Böyle bir sınıf için uygun bir yer örneğin app/Forms dizinidir.
Factory sınıfı şöyle görünebilir:
use Nette\Application\UI\Form;
class SignInFormFactory
{
public function create(): Form
{
$form = new Form;
$form->addText('name', 'Ad:');
$form->addSubmit('send', 'Giriş yap');
return $form;
}
}
Formu üreten sınıfı, presenter'daki bileşen factory metodunda isteriz:
public function __construct(
private SignInFormFactory $formFactory,
) {
}
protected function createComponentSignInForm(): Form
{
$form = $this->formFactory->create();
// formu değiştirebiliriz, burada örneğin düğmedeki metni değiştiriyoruz
$form['send']->setCaption('Devam et');
$form->onSuccess[] = $this->signInFormSuceeded(...); // ve işleyici ekliyoruz
return $form;
}
Form işleme işleyicisi factory'nin kendisi tarafından da sağlanabilir:
use Nette\Application\UI\Form;
class SignInFormFactory
{
public function create(): Form
{
$form = new Form;
$form->addText('name', 'Ad:');
$form->addSubmit('send', 'Giriş yap');
$form->onSuccess[] = function (Form $form, $data): void {
// gönderilen formumuzu burada işliyoruz
};
return $form;
}
}
Böylece Nette'te formlara hızlı bir girişi tamamladık. Daha fazla ilham için dağıtımdaki örnekler dizinine bakmayı deneyin.