Etkileşimli bileşenler
Bileşenler, sayfalara gömdüğümüz ayrı ve yeniden kullanılabilir nesnelerdir. Formlar, datagrid'ler, anketler, kısacası tekrar tekrar kullanmanın anlamlı olduğu her şey olabilirler. Şunları göstereceğiz:
- bileşenler nasıl kullanılır?
- nasıl yazılır?
- sinyaller nedir?
Nette'in yerleşik bir bileşen sistemi vardır. Buna benzer bir şey Delphi veya ASP.NET Web Forms kıdemlilerine tanıdık gelebilir; React ya da Vue.js de uzaktan benzer bir şey üzerine kuruludur. Ancak PHP framework'leri dünyasında bu eşsiz bir özelliktir.
Aynı zamanda bileşenler, uygulama geliştirmeye yaklaşımı temelden etkiler. Sayfaları önceden hazırlanmış birimlerden oluşturabilirsiniz. Yönetim panelinizde bir datagrid mi lazım? Onu, Nette için açık kaynak eklentilerin (yalnızca bileşenlerin değil) deposu olan Componette üzerinde bulun ve presenter'a eklemeniz yeterli.
Presenter'a istediğiniz sayıda bileşen yerleştirebilirsiniz. Bazı bileşenlerin içine de başka bileşenler gömebilirsiniz. Böylece kökü presenter olan bir bileşen ağacı oluşur.
Factory metotları
Bileşenler presenter'a nasıl yerleştirilir ve sonra nasıl kullanılır? Genellikle factory metotlarıyla.
Bileşen factory'si, bileşenleri yalnızca gerçekten gerektiğinde (lazy / talep üzerine) oluşturmanın zarif bir yoludur.
Tüm sihir, createComponent<Ad>() adlı bir metodu uygulamakta yatar; burada <Ad>
oluşturulan bileşenin adıdır ve metot bileşeni oluşturup döndürür.
class DefaultPresenter extends Nette\Application\UI\Presenter
{
protected function createComponentPoll(): PollControl
{
$poll = new PollControl;
$poll->items = $this->items;
return $poll;
}
}
Tüm bileşenler ayrı metotlarda oluşturulduğu için kod daha anlaşılır olur.
Bileşen adları, metot adında büyük harfle yazılsalar da, her zaman küçük harfle başlar.
Factory'leri asla doğrudan çağırmayız; bileşeni ilk kullandığımızda otomatik çağrılırlar. Bu sayede bileşen doğru anda ve yalnızca gerçekten gerekiyorsa oluşturulur. Bileşeni kullanmazsak (örneğin sayfanın yalnızca bir bölümünün aktarıldığı bir AJAX isteğinde veya şablon önbelleğe alınırken), hiç oluşturulmaz ve sunucu performansından tasarruf edilir.
// bileşene erişiriz ve bu ilk seferse
// onu oluşturan createComponentPoll() çağrılır
$poll = $this->getComponent('poll');
// alternatif sözdizimi: $poll = $this['poll'];
Şablonda bir bileşen {control} etiketiyle render edilebilir. Bu yüzden bileşenleri şablona elle aktarmaya gerek yoktur.
<h2>Lütfen oy verin</h2>
{control poll}
Sayısı değişken bileşenleri dinamik olarak oluşturmak için Multiplier kullanın.
createComponent<Ad>() factory metotları yalnızca presenter'larda çalışmaz. Bir bileşeni başka bir
bileşenin içine aynı şekilde yerleştirip ağaç halinde birleştirebilirsiniz; bu, örneğin bir bileşenin içinde ayrı
render edilen bir form için kullanışlıdır.
Hollywood tarzı
Bileşenler genellikle Hollywood tarzı demeyi sevdiğimiz taze bir teknik kullanır. Film seçmelerine katılanların sıkça duyduğu klişeyi mutlaka bilirsiniz: “Bizi aramayın, biz sizi ararız.” İşte tam olarak mesele budur.
Nette'te sürekli soru sormak zorunda kalmak yerine (“form gönderildi mi?”, “geçerli miydi?”, “kullanıcı bu düğmeye bastı mı?”), framework'e “bu olduğunda şu metodu çağır” dersiniz ve gerisini ona bırakırsınız. JavaScript programlıyorsanız bu programlama tarzına yakından aşinasınızdır. Belirli bir olay gerçekleştiğinde çağrılan fonksiyonlar yazarsınız. Ve dil onlara uygun parametreleri aktarır.
Bu, uygulama yazmaya bakışı tamamen değiştirir. Framework'e ne kadar çok işi bırakabilirseniz, o kadar az işiniz olur. Ve o kadar az şeyi gözden kaçırırsınız.
Bileşen yazma
Bileşen terimiyle genellikle Nette\Application\UI\Control sınıfının
bir torununu kastediyoruz. (Aslında “control” terimini kullanmak daha doğru olurdu, ama bunun bazı dillerde başka bir
anlamı var ve “bileşen” daha çok yerleşti.) Presenter'ın kendisi, Nette\Application\UI\Presenter, de
Control sınıfının bir torunudur.
use Nette\Application\UI\Control;
class PollControl extends Control
{
}
Render
Bir bileşeni render etmek için {control bilesenAdi} etiketinin kullanıldığını zaten biliyoruz. Bu aslında
bileşenin render() metodunu çağırır; render'ı orada biz üstleniriz. Tıpkı presenter'daki gibi,
$this->template değişkeninde parametre aktardığımız bir Latte şablonu elimizin altındadır. Presenter'dan farklı olarak,
şablon dosyasını belirtmemiz ve render ettirmemiz gerekir:
public function render(): void
{
// şablona bazı parametreler ekle
$this->template->param = $value;
// ve render et
$this->template->render(__DIR__ . '/poll.latte');
}
{control} etiketi, render() metoduna parametre aktarmayı sağlar:
{control poll $id, $message}
public function render(int $id, string $message): void
{
// ...
}
Bazen bir bileşen, ayrı ayrı render etmek istediğimiz birkaç bölümden oluşabilir. Her biri için kendi render
metodumuzu oluştururuz, burada örnekte renderPaginator():
public function renderPaginator(): void
{
// ...
}
Şablonda ise onu şöyle çağırırız:
{control poll:paginator}
Daha iyi anlamak için bu etiketin PHP koduna nasıl çevrildiğini bilmek iyidir.
{control poll}
{control poll:paginator 123, 'hello'}
şuna çevrilir:
$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');
getComponent() metodu poll bileşenini döndürür ve bu bileşen üzerinde render()
metodu, ya da etikette iki nokta üst üstenin ardından farklı bir render metodu belirtilmişse renderPaginator(),
çağrılır.
Dikkat: parametrelerde köşeli parantez dışında => geçerse, tüm parametreler bir
diziye sarılır ve ilk argüman olarak aktarılır:
{control poll, id: 123, message: 'hello'}
şuna çevrilir:
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
Bir alt bileşenin render edilmesi:
{control cartControl-someForm}
şuna çevrilir:
$control->getComponent("cartControl-someForm")->render();
Bileşenler, presenter'lar gibi, şablonlara birkaç yararlı değişkeni otomatik aktarır:
$basePath, kök dizine giden mutlak URL yoludur (örneğin/eshop)$baseUrl, kök dizine giden mutlak URL'dir (örneğinhttp://localhost/eshop)$user, kullanıcıyı temsil eden bir nesnedir$presenter, geçerli presenter'dır$control, geçerli bileşendir$flashes,flashMessage()fonksiyonuyla gönderilen mesajların dizisidir
Sinyal
Bir Nette uygulamasında gezinmenin, Presenter:eylem çiftlerine bağlantı vermekten veya yönlendirmekten
oluştuğunu zaten biliyoruz. Peki ya yalnızca geçerli sayfada bir eylem gerçekleştirmek istersek? Örneğin bir
tablodaki sütunların sıralamasını değiştirmek; bir öğeyi silmek; açık/koyu modu değiştirmek; bir formu göndermek;
bir ankette oy vermek vb.
Bu tür isteğe sinyal denir. Ve eylemler action<Eylem>() veya render<Eylem>()
metotlarını çağırdığı gibi, sinyaller de handle<Sinyal>() metotlarını çağırır. Eylem (veya
görünüm) kavramı yalnızca presenter'ları ilgilendirirken, sinyaller tüm bileşenleri ilgilendirir. Dolayısıyla
presenter'ları da, çünkü UI\Presenter, UI\Control'ün torunudur.
public function handleClick(int $x, int $y): void
{
// ... sinyalin işlenmesi ...
}
Sinyal çağıran bir bağlantı her zamanki gibi oluşturulur; yani şablonda n:href niteliğiyle veya
{link} etiketiyle, kodda link() metoduyla. Daha fazlası URL bağlantıları oluşturma
bölümünde.
<a n:href="click! $x, $y">buraya tıkla</a>
Sinyal her zaman geçerli presenter ve eylem üzerinde çağrılır; başka bir presenter veya eylem üzerinde çağırmak mümkün değildir.
Böylece sinyal, sayfanın tıpkı özgün istekteki gibi yeniden yüklenmesine yol açar, ama ek olarak sinyal işleme metodunu uygun parametrelerle çağırır. Metot yoksa, kullanıcıya 403 Forbidden hata sayfası olarak gösterilen bir Nette\Application\UI\BadSignalException istisnası fırlatılır.
Snippet'ler ve AJAX
Sinyaller size biraz AJAX'ı hatırlatabilir: geçerli sayfada çağrılan işleyiciler. Ve haklısınız, sinyaller gerçekten sıklıkla AJAX ile çağrılır ve ardından tarayıcıya yalnızca sayfanın değişen bölümleri aktarılır. Bunlara snippet denir. Daha fazla bilgiyi AJAX'a ayrılmış sayfada bulabilirsiniz.
Flash mesajları
Bir bileşenin, presenter'dan bağımsız kendi flash mesaj deposu vardır. Bunlar örneğin bir işlemin sonucunu bildiren mesajlardır. Flash mesajlarının önemli bir özelliği, yönlendirmeden sonra da şablonda erişilebilir olmalarıdır. Gösterildikten sonra bile 30 saniye daha etkin kalırlar; örneğin kullanıcı bir aktarım hatası yüzünden sayfayı yenilerse mesaj hemen kaybolmaz.
Gönderimi flashMessage metodu
üstlenir. İlk parametre mesajın metni (string, Stringable) veya mesajı temsil eden bir
stdClass nesnesidir. İsteğe bağlı ikinci parametre onun tipidir (error, warning, info vb.).
flashMessage() metodu, flash mesajın örneğini bir stdClass nesnesi olarak döndürür; ona başka
bilgiler eklenebilir.
$this->flashMessage('Öğe silindi.');
$this->redirect(/* ... */); // ve yönlendir
Bu mesajlar şablona $flashes değişkeninde stdClass nesneleri olarak sunulur; message
(mesaj metni) ve type (mesaj tipi) özelliklerini içerirler ve sözü geçen kullanıcı bilgilerini de
taşıyabilirler. Onları örneğin şöyle render ederiz:
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Sinyalin işlenmesinden sonra yönlendirme
Bir bileşenin sinyalinin işlenmesinin ardından çoğu zaman bir yönlendirme gelir. Bu formlara benzer; onları gönderdikten sonra da, sayfa tarayıcıda yenilenirse verinin yeniden gönderilmesini engellemek için yönlendiririz.
$this->redirect('this'); // geçerli presenter ve eyleme yönlendirir
Bir bileşen yeniden kullanılabilir bir öğe olduğundan ve genellikle belirli presenter'larla doğrudan bağı olmaması
gerektiğinden, redirect() ve link() metotları parametreyi otomatik olarak bileşen sinyali diye
yorumlar:
$this->redirect('click'); // aynı bileşenin 'click' sinyaline yönlendirir
Başka bir presenter'a veya eyleme yönlendirmeniz gerekirse bunu presenter üzerinden yapabilirsiniz:
$this->getPresenter()->redirect('Product:show'); // başka bir presenter'a/eyleme yönlendirir
Kalıcı parametreler
Kalıcı parametreler, bileşenlerdeki durumu farklı istekler boyunca korumak için kullanılır. Değerleri, bir bağlantıya tıklandıktan sonra da aynı kalır. Oturum verilerinin aksine URL'de aktarılırlar. Ve bu, aynı sayfadaki başka bileşenlerde oluşturulan bağlantılar dahil, tamamen otomatik gerçekleşir.
Örneğin içeriği sayfalamak için bir bileşeniniz var. Bir sayfada böyle birkaç bileşen olabilir. Ve bir bağlantıya
tıklandıktan sonra tüm bileşenlerin kendi geçerli sayfalarında kalmasını istiyoruz. Bu yüzden sayfa numarasını
(page) kalıcı parametre yaparız.
Nette'te kalıcı parametre oluşturmak son derece basittir. Public bir özellik oluşturup onu nitelikle işaretlemeniz
yeterlidir: (önceden /** @persistent */ kullanılıyordu)
use Nette\Application\Attributes\Persistent; // bu satır önemli
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1; // public olmalı
}
Özellik için veri tipini (örneğin int) belirtmenizi öneririz, ayrıca bir varsayılan değer de
verebilirsiniz. Parametre değerleri doğrulanabilir.
Bağlantı oluştururken kalıcı parametrenin değeri değiştirilebilir:
<a n:href="this page: $page + 1">sonraki</a>
Ya da sıfırlanabilir, yani URL'den kaldırılabilir. O zaman varsayılan değerini alır:
<a n:href="this page: null">sıfırla</a>
Kalıcı bileşenler
Yalnızca parametreler değil, bileşenler de kalıcı olabilir. Kalıcı parametreleri o zaman presenter'ın farklı
eylemleri arasında, hatta birden fazla presenter arasında bile aktarılır. Kalıcı bileşenleri presenter sınıfı üzerinde
bir nitelikle işaretleriz. Örneğin calendar ve poll bileşenlerini şöyle işaretleriz:
use Nette\Application\Attributes\Persistent;
#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Bu bileşenlerin içindeki alt bileşenleri işaretlemeye gerek yoktur; onlar da kalıcı olur.
Eski @persistent anotasyonu hâlâ çalışır, ama kullanımdan kaldırılmıştır ve bir uyarı tetikler:
/**
* @persistent(calendar, poll)
*/
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Bağımlılıkları olan bileşenler
Bağımlılıkları olan bileşenler, onları kullanacak presenter'ları “karıştırmadan” nasıl oluşturulur? Nette'teki DI konteynerinin akıllı özellikleri sayesinde, klasik servislerin kullanımında olduğu gibi, işin çoğu framework'e bırakılabilir.
PollFacade servisine bağımlılığı olan bir bileşen örneğini ele alalım:
class PollControl extends Control
{
public function __construct(
private int $id, // bileşeni oluşturduğumuz anketin ID'si
private PollFacade $facade,
) {
}
public function handleVote(int $voteId): void
{
$this->facade->vote($this->id, $voteId);
// ...
}
}
Klasik bir servis yazsaydık tartışacak bir şey olmazdı. DI konteyneri tüm bağımlılıkların aktarımını görünmez
biçimde üstlenirdi. Ancak bileşenlerde bunları genellikle, presenter'da doğrudan Factory
metotları createComponent…() içinde yeni bir örnek oluşturarak hallederiz. Ama tüm bileşenlerin tüm
bağımlılıklarını, yalnızca bileşenlere aktarmak için presenter'a aktarmak zahmetlidir. Bir de yazılacak kod
miktarı…
Mantıklı soru şu: neden bileşeni klasik bir servis olarak kaydedip presenter'a aktarmıyor ve sonra
createComponent…() metodunda döndürmüyoruz? Ancak bu yaklaşım uygun değildir, çünkü gerektiğinde
bileşeni birden fazla kez oluşturabilme olanağını isteriz.
Doğru çözüm, bileşen için bir factory, yani bileşeni bizim için oluşturan bir sınıf yazmaktır:
class PollControlFactory
{
public function __construct(
private PollFacade $facade,
) {
}
public function create(int $id): PollControl
{
return new PollControl($id, $this->facade);
}
}
Bu factory'yi yapılandırmada konteynerimize kaydederiz:
services:
- PollControlFactory
ve son olarak presenter'ımızda kullanırız:
class PollPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private PollControlFactory $pollControlFactory,
) {
}
protected function createComponentPollControl(): PollControl
{
$pollId = 1; // kendi parametremizi aktarabiliriz
return $this->pollControlFactory->create($pollId);
}
}
İşin güzel yanı, Nette DI'nin böyle basit factory'leri üretebilmesidir; yani tüm kodunu yazmak yerine yalnızca arayüzünü yazmanız yeterli:
interface PollControlFactory
{
public function create(int $id): PollControl;
}
Ve hepsi bu. Nette bu arayüzü içeride uygular ve onu presenter'a enjekte eder, biz de orada kullanırız. $id
parametresini ve PollFacade sınıfının bir örneğini bileşenimize sihirli biçimde ekler.
Bileşenler derinlemesine
Nette Application'daki bileşenler, sayfalara gömdüğümüz ve bu bölümün tamamının ayrıldığı, web uygulamasının yeniden kullanılabilir parçalarıdır. Peki böyle bir bileşenin yetenekleri tam olarak nelerdir?
- Bir şablonda render edilebilir
- Bir AJAX isteğinde kendisinin hangi bölümünü render edeceğini bilir (snippet'ler)
- Durumunu URL'de saklama yeteneğine sahiptir (kalıcı parametreler)
- Kullanıcı eylemlerine tepki verme yeteneğine sahiptir (sinyaller)
- Hiyerarşik bir yapı oluşturur (kökü presenter'dır)
Bu işlevlerin her birini kalıtım zincirindeki sınıflardan biri üstlenir. Render'ı (1 + 2) Nette\Application\UI\Control, yaşam döngüsüne entegrasyonu (3, 4) Nette\Application\UI\Component sınıfı, hiyerarşik yapının oluşturulmasını (5) ise Container ve Component sınıfları üstlenir.
Nette\ComponentModel\Component { IComponent }
|
+- Nette\ComponentModel\Container { IContainer }
|
+- Nette\Application\UI\Component { SignalReceiver, StatePersistent }
|
+- Nette\Application\UI\Control { Renderable }
|
+- Nette\Application\UI\Presenter { IPresenter }
Bileşenin yaşam döngüsü
Kalıcı parametrelerin doğrulanması
URL'lerden alınan Kalıcı parametreler değerleri özelliklere
loadState() metoduyla yazılır. Bu metot ayrıca özellik için belirtilen veri tipinin uyup uymadığını
denetler; uymuyorsa 404 hatasıyla yanıt verir ve sayfa gösterilmez.
Kalıcı parametrelere asla körü körüne güvenmeyin, çünkü kullanıcı onları URL'de kolayca değiştirebilir.
Örneğin sayfa numarası $this->page'in 0'dan büyük olup olmadığını böyle denetleriz. Uygun bir yol,
sözü geçen loadState() metodunu ezmektir:
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1;
public function loadState(array $params): void
{
parent::loadState($params); // $this->page burada ayarlanır
// ardından kendi değer denetimimiz gelir:
if ($this->page < 1) {
$this->error();
}
}
}
Tersi süreci, yani değerlerin kalıcı özelliklerden toplanmasını, saveState() metodu üstlenir.
Presenter'a bağlanma
Bir bileşen presenter hiyerarşisinin parçası olduğu anda, $onAnchor dizisinde saklanan callback'leri
çağrılır. O andan itibaren bileşenin presenter'ı elinin altındadır, güvenle bağlantı oluşturabilir, kalıcı
parametreleri okuyabilir vb.
$control->onAnchor[] = function ($control): void {
// bileşenin artık presenter'ı elinin altında
};
Sinyaller derinlemesine
Bir sinyal, sayfanın tıpkı özgün istekteki gibi yeniden yüklenmesine yol açar (AJAX ile çağrılması dışında) ve
varsayılan uygulaması Nette\Application\UI\Component sınıfında bulunan signalReceived($signal)
metodunu çağırır; bu uygulama handle<Sinyal> sözcüklerinden oluşan bir metodu çağırmayı dener.
Sonraki işleme ilgili nesneye kalmıştır. Component'ten kalıtım alan nesneler (yani Control ve
Presenter), handle<Sinyal> metodunu uygun parametrelerle çağırmayı deneyerek tepki verir.
Başka bir deyişle: handle<Sinyal> fonksiyonunun tanımı, istekle gelen tüm parametrelerle birlikte
alınır, URL'deki parametreler argümanlara ada göre atanır ve metot çağrılmaya çalışılır. Örneğin URL'deki
id parametresinin değeri $id argümanı olarak, URL'deki something ise
$something olarak aktarılır vb. Metot yoksa signalReceived metodu bir istisna fırlatır.
Bir sinyal, URL'deki parametrelerin yanı sıra isteğin POST gövdesinde gönderilen parametreleri de okur. Bu işe yarar, çünkü sinyaller sıklıkla JavaScript ile çağrılır ve orada veriyi POST yöntemiyle göndermek doğaldır. Ancak aynı adlı bir parametre hem URL'den hem POST gövdesinden gelirse, URL'deki değer önceliklidir. Bu yüzden bir POST alanına URL veya rota parametresiyle aynı adı vermekten kaçının, yoksa URL değeri onu sessizce ezer. Sinyal parametreleri, eylem ve kalıcı parametrelerle ortak bir alanı paylaşır; bkz. Ortak parametre alanı.
Bir sinyali, SignalReceiver arayüzünü uygulayan ve bileşen ağacına bağlı olan her bileşen, presenter veya
nesne alabilir.
Sinyallerin başlıca alıcıları Presenter'lar ve Control'den kalıtım alan görsel bileşenler
olacaktır. Sinyal, bir nesneye bir şey yapması gerektiğinin işareti olmayı amaçlar: bir anket kullanıcının oyunu
saymalı, bir haber bloğu genişleyip iki kat fazla haber göstermeli, bir form gönderildi ve veriyi işlemeli vb.
Sinyalin URL'si Component::link() metoduyla
oluşturulur. $destination parametresi olarak {sinyal}! dizesini, $args olarak ise sinyale
aktarmak istediğimiz argümanların dizisini veririz. Sinyal her zaman geçerli presenter ve eylem üzerinde, geçerli
parametrelerle çağrılır; sinyal parametreleri yalnızca eklenir. Ayrıca sinyali belirten ?do parametresi
eklenir.
Biçimi ya {sinyal} ya da {sinyalAlicisi}-{sinyal}'dir. {sinyalAlicisi}, presenter'daki
bileşenin adıdır. Bu yüzden bileşen adında kısa çizgi kullanılamaz; o, bileşen adını ve sinyali ayırmaya yarar, ama
bu yolla birden fazla bileşeni iç içe geçirmek mümkündür.
isSignalReceiver()
metodu, bileşenin (ilk argüman) sinyalin (ikinci argüman) alıcısı olup olmadığını denetler. İkinci argüman
atlanabilir; o zaman bileşenin herhangi bir sinyalin alıcısı olup olmadığı denetlenir. İkinci parametre true
yapılırsa, belirtilen bileşenin veya torunlarından herhangi birinin alıcı olup olmadığı doğrulanır.
handle<Sinyal>'den önceki herhangi bir aşamada, sinyali processSignal()
metodunu çağırarak elle çalıştırabiliriz; bu metot sinyalin işlenmesini üstlenir: sinyalin alıcısı olarak belirlenen
bileşeni alır (alıcı belirtilmemişse presenter'ın kendisidir) ve sinyali ona gönderir.
Örnek:
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
Bu, sinyali erkenden çalıştırır ve sinyal bir daha çağrılmaz.