Etkileşimli Bileşenler
Bileşenler, sayfalara eklediğimiz bağımsız, yeniden kullanılabilir nesnelerdir. Bunlar formlar, veri ızgaraları, anketler, aslında tekrar tekrar kullanılması mantıklı olan her şey olabilir. Şunları göstereceğiz:
- bileşenler nasıl kullanılır?
- nasıl yazılır?
- sinyaller nedir?
Nette'nin yerleşik bir bileşen sistemi vardır. Delphi veya ASP.NET Web Forms'tan aşina olanlar benzer bir şey hatırlayabilir, React veya Vue.js de uzaktan benzer bir şeye dayanmaktadır. Ancak, PHP framework dünyasında bu benzersiz bir özelliktir.
Bununla birlikte, bileşenler uygulama geliştirme yaklaşımını temelden etkiler. Sayfaları önceden hazırlanmış birimlerden oluşturabilirsiniz. Yönetimde bir veri ızgarasına mı ihtiyacınız var? Onu Nette için açık kaynaklı eklentilerin (yani sadece bileşenlerin değil) deposu olan Componette adresinde bulabilir ve presenter'a kolayca ekleyebilirsiniz.
Presenter'a istediğiniz sayıda bileşen ekleyebilirsiniz. Ve bazı bileşenlere başka bileşenler ekleyebilirsiniz. Bu, kökü presenter olan bir bileşen ağacı oluşturur.
Fabrika Metotları
Bileşenler presenter'a nasıl eklenir ve ardından kullanılır? Genellikle fabrika metotları aracılığıyla.
Bileşen fabrikası, bileşenleri yalnızca gerçekten ihtiyaç duyulduğunda (lazy / on demand) oluşturmanın zarif bir
yoludur. Tüm sihir, <Name>
öğesinin oluşturulan bileşenin adı olduğu ve bileşeni oluşturup döndüren
createComponent<Name>()
adlı bir metodun uygulanmasında yatar.
class DefaultPresenter extends Nette\Application\UI\Presenter
{
protected function createComponentPoll(): PollControl
{
$poll = new PollControl;
$poll->items = $this->item;
return $poll;
}
}
Tüm bileşenlerin ayrı metotlarda oluşturulması sayesinde kod daha okunaklı hale gelir.
Bileşen adları, metot adında büyük harfle yazılsa bile her zaman küçük harfle başlar.
Fabrikaları asla doğrudan çağırmayız, bileşeni ilk kullandığımızda kendiliğinden çağrılırlar. Bu sayede bileşen doğru zamanda ve yalnızca gerçekten ihtiyaç duyulduğunda oluşturulur. Bileşeni kullanmazsak (örneğin, sayfanın yalnızca bir kısmının aktarıldığı bir AJAX isteğinde veya şablon önbelleğe alınırken), hiç oluşturulmaz ve sunucu performansından tasarruf ederiz.
// bileşene erişiriz ve eğer ilk kez ise,
// onu oluşturan createComponentPoll() çağrılır
$poll = $this->getComponent('poll');
// alternatif sözdizimi: $poll = $this['poll'];
Şablonda, bir bileşeni {control} etiketi kullanarak oluşturmak mümkündür. Bu nedenle bileşenleri şablona manuel olarak iletmeye gerek yoktur.
<h2>Oy Verin</h2>
{control poll}
Hollywood Tarzı
Bileşenler genellikle Hollywood tarzı dediğimiz yeni bir tekniği kullanır. Film seçmelerine katılanların sıkça duyduğu şu meşhur cümleyi mutlaka bilirsiniz: “Bizi aramayın, biz sizi ararız.” İşte tam olarak bundan bahsediyoruz.
Nette'de, sürekli bir şeyler sormak yerine (“form gönderildi mi?”, “geçerli miydi?” veya “kullanıcı bu düğmeye bastı mı?”), framework'e “bu olduğunda, şu metodu çağır” dersiniz ve geri kalan işi ona bırakırsınız. JavaScript ile programlama yapıyorsanız, bu programlama tarzına aşinasınızdır. Belirli bir olay gerçekleştiğinde çağrılan fonksiyonlar yazarsınız. Ve dil onlara ilgili parametreleri iletir.
Bu, uygulama yazma şeklini tamamen değiştirir. Framework'e ne kadar çok görev bırakabilirseniz, o kadar az işiniz olur. Ve dolayısıyla gözden kaçırabileceğiniz şeyler de o kadar azalır.
Bileşen Yazma
Bileşen terimiyle genellikle Nette\Application\UI\Control sınıfının
bir alt sınıfını kastederiz. (Dolayısıyla “kontroller” terimini kullanmak daha doğru olurdu, ancak “kontroller”
Türkçede tamamen farklı bir anlama sahiptir ve “bileşenler” daha çok yerleşmiştir.) Bu arada, presenter Nette\Application\UI\Presenter kendisi de
Control
sınıfının bir alt sınıfıdır.
use Nette\Application\UI\Control;
class PollControl extends Control
{
}
Oluşturma
Biliyoruz ki bir bileşeni oluşturmak için {control componentName}
etiketi kullanılır. Bu aslında bileşenin
render()
metodunu çağırır ve burada oluşturmayı biz hallederiz. Tıpkı presenter'da olduğu gibi,
$this->template
değişkeninde Latte şablonuna
sahibiz ve ona parametreleri iletiriz. Presenter'dan farklı olarak, şablon dosyasını belirtmeli ve oluşturulmasını
sağlamalıyız:
public function render(): void
{
// şablona bazı parametreler ekleriz
$this->template->param = $value;
// ve onu oluştururuz
$this->template->render(__DIR__ . '/poll.latte');
}
{control}
etiketi, render()
metoduna parametreler iletmeyi sağlar:
{control poll $id, $message}
public function render(int $id, string $message): void
{
// ...
}
Bazen bir bileşen, ayrı ayrı oluşturmak istediğimiz birkaç bölümden oluşabilir. Her biri için kendi oluşturma
metodumuzu oluştururuz, buradaki örnekte örneğin renderPaginator()
:
public function renderPaginator(): void
{
// ...
}
Ve şablonda onu şu şekilde çağırırız:
{control poll:paginator}
Daha iyi anlamak için, bu etiketin PHP'ye nasıl çevrildiğini bilmek iyidir.
{control poll}
{control poll:paginator 123, 'hello'}
şu şekilde ç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()
metodunu veya etikette iki noktadan sonra farklı bir oluşturma yöntemi belirtilmişse renderPaginator()
metodunu
çağırır.
Dikkat, parametrelerin herhangi bir yerinde =>
görünürse, tüm parametreler bir
diziye paketlenir ve ilk argüman olarak iletilir:
{control poll, id: 123, message: 'hello'}
şu şekilde çevrilir:
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
Alt bileşenin oluşturulması:
{control cartControl-someForm}
şu şekilde çevrilir:
$control->getComponent("cartControl-someForm")->render();
Bileşenler, presenter'lar gibi, şablonlara otomatik olarak birkaç yararlı değişken iletir:
$basePath
, kök dizine mutlak URL yoludur (örn./eshop
)$baseUrl
, kök dizine mutlak URL'dir (örn.http://localhost/eshop
)$user
, kullanıcıyı temsil eden nesnedir$presenter
, mevcut presenter'dır$control
, mevcut bileşendir$flashes
,flashMessage()
fonksiyonu tarafından gönderilen mesajlar dizisidir
Sinyal
Nette uygulamasında gezinmenin Presenter:action
çiftlerine bağlantı verme veya yönlendirme yapmaktan ibaret
olduğunu zaten biliyoruz. Peki ya sadece mevcut 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 form göndermek; bir ankette
oy kullanmak; vb.
Bu tür isteklere sinyal denir. Ve eylemlerin action<Action>()
veya render<Action>()
metotlarını tetiklemesi gibi, sinyaller de handle<Signal>()
metotlarını çağırır. Eylem (veya view)
kavramı yalnızca presenter'larla ilgiliyken, sinyaller tüm bileşenlerle ilgilidir. Ve dolayısıyla presenter'larla da,
çünkü UI\Presenter
, UI\Control
'un bir alt sınıfıdır.
public function handleClick(int $x, int $y): void
{
// ... sinyalin işlenmesi ...
}
Sinyali çağıran bir bağlantıyı normal şekilde oluştururuz, yani şablonda n:href
niteliğiyle veya
{link}
etiketiyle, kodda link()
metoduyla. Daha fazla bilgi için URL Bağlantıları Oluşturma
bölümüne bakın.
<a n:href="click! $x, $y">buraya tıkla</a>
Sinyal her zaman mevcut presenter ve eylem üzerinde çağrılır, başka bir presenter veya başka bir eylem üzerinde çağrılamaz.
Dolayısıyla sinyal, sayfanın orijinal istekteki gibi tamamen yeniden yüklenmesine neden olur, ancak ek olarak ilgili parametrelerle sinyal işleyici metodunu çağırır. Metot mevcut değilse, kullanıcıya 403 Forbidden hata sayfası olarak gösterilen Nette\Application\UI\BadSignalException istisnası atılır.
Snippet'ler ve AJAX
Sinyaller size biraz AJAX'ı hatırlatabilir: mevcut sayfada çağrılan işleyiciler. Ve haklısınız, sinyaller gerçekten de sık sık AJAX kullanılarak çağrılır ve ardından sayfanın yalnızca değiştirilmiş bölümleri tarayıcıya aktarılır. Yani sözde snippet'ler. Daha fazla bilgi için AJAX'a ayrılmış sayfada bulabilirsiniz.
Flash Mesajları
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 bile şablonda kullanılabilir olmalarıdır. Görüntülendikten sonra bile 30 saniye daha canlı kalırlar – örneğin, hatalı bir aktarım nedeniyle kullanıcının sayfayı yenilemesi durumunda mesaj hemen kaybolmaz.
Gönderme işlemi flashMessage metodu
tarafından gerçekleştirilir. İlk parametre mesaj metni veya mesajı temsil eden bir stdClass
nesnesidir. İsteğe
bağlı ikinci parametre türüdür (error, warning, info vb.). flashMessage()
metodu, flash mesajının bir
örneğini stdClass
nesnesi olarak döndürür ve buna ek bilgiler eklenebilir.
$this->flashMessage('Öğe silindi.');
$this->redirect(/* ... */); // ve yönlendiririz
Şablonda bu mesajlar $flashes
değişkeninde stdClass
nesneleri olarak bulunur ve
message
(mesaj metni), type
(mesaj türü) özelliklerini içerir ve daha önce bahsedilen kullanıcı
bilgilerini içerebilir. Onları örneğin şu şekilde oluştururuz:
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Sinyal Sonrası Yönlendirme
Bileşen sinyallerinin işlenmesinden sonra genellikle bir yönlendirme takip eder. Bu, formlardaki duruma benzer – gönderildikten sonra da yönlendiririz, böylece tarayıcıda sayfa yenilendiğinde veriler tekrar gönderilmez.
$this->redirect('this') // mevcut presenter ve eyleme yönlendirir
Bileşen yeniden kullanılabilir bir öğe olduğundan ve genellikle belirli presenter'larla doğrudan bir bağlantısı
olmaması gerektiğinden, redirect()
ve link()
metotları parametreyi otomatik olarak bileşen sinyali
olarak yorumlar:
$this->redirect('click') // aynı bileşenin 'click' sinyaline yönlendirir
Başka bir presenter'a veya eyleme yönlendirmeniz gerekiyorsa, bunu presenter aracılığıyla yapabilirsiniz:
$this->getPresenter()->redirect('Product:show'); // başka bir presenter/eyleme yönlendirir
Kalıcı Parametreler
Kalıcı parametreler, farklı istekler arasında bileşenlerde durumu korumak için kullanılır. Değerleri, bir bağlantıya tıklandıktan sonra bile aynı kalır. Oturumdaki verilerin aksine, URL'de aktarılırlar. Ve bu tamamen otomatiktir, aynı sayfadaki diğer bileşenlerde oluşturulan bağlantılar dahil.
Örneğin, içeriği sayfalandırmak için bir bileşeniniz var. Sayfada bu tür birkaç bileşen olabilir. Ve bir bağlantıya
tıklandığında tüm bileşenlerin mevcut sayfalarında kalmasını istiyoruz. Bu nedenle, sayfa numarasını
(page
) kalıcı bir parametre yaparız.
Nette'de kalıcı bir parametre oluşturmak son derece basittir. Sadece genel bir özellik oluşturmanız ve onu bir nitelikle
işaretlemeniz yeterlidir: (eskiden /** @persistent */
kullanılırdı)
use Nette\Application\Attributes\Persistent; // bu satır önemlidir
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1; // public olmalı
}
Özellik için veri türünü (örn. int
) belirtmenizi öneririz ve varsayılan bir değer de belirtebilirsiniz.
Parametre değerleri doğrulanabilir.
Bir bağlantı oluştururken, kalıcı parametrenin değeri değiştirilebilir:
<a n:href="this page: $page + 1">sonraki</a>
Veya sıfırlanabilir, yani URL'den kaldırılabilir. O zaman varsayılan değerini alacaktır:
<a n:href="this page: null">sıfırla</a>
Kalıcı Bileşenler
Sadece parametreler değil, bileşenler de kalıcı olabilir. Böyle bir bileşenin kalıcı parametreleri, presenter'ın
farklı eylemleri arasında veya birden fazla presenter arasında da aktarılır. Kalıcı bileşenleri presenter sınıfındaki
bir ek açıklama ile işaretleriz. Örneğin, calendar
ve poll
bileşenlerini şu şekilde
işaretleriz:
/**
* @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ı hale gelirler.
PHP 8'de, kalıcı bileşenleri işaretlemek için nitelikleri de kullanabilirsiniz:
use Nette\Application\Attributes\Persistent;
#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Bağımlılıklara Sahip Bileşenler
Bağımlılıklara sahip bileşenleri, onları kullanacak presenter'ları “kirletmeden” nasıl oluşturabiliriz? Nette'deki DI konteynerinin akıllı özellikleri sayesinde, klasik servisleri kullanırken olduğu gibi, işin çoğunu framework'e bırakabiliriz.
Örnek olarak, PollFacade
servisine bağımlılığı olan bir bileşeni ele alalım:
class PollControl extends Control
{
public function __construct(
private int $id, // Bileşeni oluşturduğumuz anketin kimliği
private PollFacade $facade,
) {
}
public function handleVote(int $voteId): void
{
$this->facade->vote($this->id, $voteId);
// ...
}
}
Klasik bir servis yazıyor olsaydık, çözülecek bir şey olmazdı. Tüm bağımlılıkların iletilmesi DI konteyneri
tarafından görünmez bir şekilde halledilirdi. Ancak bileşenlerle genellikle, yeni örneklerini doğrudan presenter'da fabrika metotlarında createComponent…()
oluşturacak şekilde çalışırız.
Ancak tüm bileşenlerin tüm bağımlılıklarını presenter'a iletmek, sonra onları bileşenlere iletmek hantaldır. Ve
yazılan kod miktarı…
Mantıksal soru şudur: neden bileşeni basitçe klasik bir servis olarak kaydetmiyor, presenter'a iletmiyor ve sonra
createComponent…()
metodunda döndürmüyoruz? Ancak bu yaklaşım uygunsuzdur, çünkü bileşeni birden çok kez
oluşturabilmek istiyoruz.
Doğru çözüm, bileşen için bir fabrika yazmaktır, yani bize bileşeni oluşturacak bir sınıf:
class PollControlFactory
{
public function __construct(
private PollFacade $facade,
) {
}
public function create(int $id): PollControl
{
return new PollControl($id, $this->facade);
}
}
Bu fabrikayı yapılandırmamızda konteynerimize kaydederiz:
services:
- PollControlFactory
ve son olarak onu 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; // parametremizi iletebiliriz
return $this->pollControlFactory->create($pollId);
}
}
Harika olan şey, Nette DI'nin bu tür basit fabrikaları oluşturabilmesidir, bu yüzden tüm kodunu yazmak yerine sadece arayüzünü yazmak yeterlidir:
interface PollControlFactory
{
public function create(int $id): PollControl;
}
Ve hepsi bu. Nette bu arayüzü dahili olarak uygular ve presenter'a iletir, burada onu zaten kullanabiliriz. Sihirli bir
şekilde $id
parametresini ve PollFacade
sınıfının bir örneğini bileşenimize ekler.
Bileşenler Derinlemesine
Nette Application'daki bileşenler, web uygulamasının yeniden kullanılabilir parçalarıdır, bunları sayfalara ekleriz ve bu bölümün tamamı onlara ayrılmıştır. Böyle bir bileşenin tam olarak hangi yetenekleri vardır?
- şablonda oluşturulabilir
- AJAX isteğinde hangi bölümünün oluşturulacağını bilir (snippet'ler)
- durumunu URL'de saklama yeteneğine sahiptir (kalıcı parametreler)
- kullanıcı eylemlerine yanıt verme yeteneğine sahiptir (sinyaller)
- hiyerarşik bir yapı oluşturur (kökü presenter'dır)
Bu fonksiyonların her biri kalıtım çizgisindeki sınıflardan biri tarafından sağlanır. Oluşturma (1 + 2) Nette\Application\UI\Control tarafından, yaşam döngüsüne dahil etme (3, 4) Nette\Application\UI\Component sınıfı tarafından ve hiyerarşik yapı oluşturma (5) Container ve Component sınıfları tarafından halledilir.
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'den alınan kalıcı parametrelerin değerleri loadState()
metodu
tarafından özelliklere yazılır. Bu metot ayrıca özellikte belirtilen veri türünün eşleşip eşleşmediğini de kontrol
eder, aksi takdirde 404 hatasıyla yanıt verir ve sayfa görüntülenmez.
Kalıcı parametrelere asla körü körüne güvenmeyin, çünkü kullanıcı tarafından URL'de kolayca üzerine
yazılabilirler. Örneğin, $this->page
sayfa numarasının 0'dan büyük olup olmadığını bu şekilde
doğrularız. Uygun yol, bahsedilen loadState()
metodunu geçersiz kılmaktır:
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1;
public function loadState(array $params): void
{
parent::loadState($params); // burada $this->page ayarlanır
// ardından kendi değer kontrolümüz gelir:
if ($this->page < 1) {
$this->error();
}
}
}
Ters işlem, yani kalıcı özelliklerden değerlerin toplanması, saveState()
metodunun sorumluluğundadır.
Sinyaller Derinlemesine
Bir sinyal, sayfanın orijinal istekteki gibi tamamen yeniden yüklenmesine neden olur (AJAX ile çağrıldığı durumlar
hariç) ve signalReceived($signal)
metodunu çağırır; bu metodun Nette\Application\UI\Component
sınıfındaki varsayılan uygulaması, handle{signal}
kelimelerinden oluşan bir metodu çağırmaya çalışır.
Daha sonraki işlemler ilgili nesneye bağlıdır. Component
'ten (yani Control
ve
Presenter
) miras alan nesneler, ilgili parametrelerle handle{signal}
metodunu çağırmaya çalışarak
yanıt verirler.
Başka bir deyişle: handle{signal}
fonksiyonunun tanımı alınır ve istekle birlikte gelen tüm parametreler
alınır ve argümanlara URL'den parametreler ada göre atanır ve ilgili metot çağrılmaya çalışılır. Örneğin,
$id
parametresi olarak URL'deki id
parametresinin değeri, $something
olarak URL'deki
something
vb. iletilir. Ve metot mevcut değilse, signalReceived
metodu bir istisna atar.
Sinyal, SignalReceiver
arayüzünü uygulayan ve bileşen ağacına bağlı olan herhangi bir bileşen, presenter
veya nesne tarafından alınabilir.
Sinyallerin ana alıcıları Presenter
'lar ve Control
'dan miras alan görsel bileşenler olacaktır.
Sinyal, nesneye bir şey yapması gerektiğine dair bir işaret görevi görmelidir – anket kullanıcının oyunu saymalı,
haber bloğu genişlemeli ve iki kat daha fazla haber göstermeli, form gönderildi ve verileri işlemeli vb.
Sinyal için URL, Component::link() metodu
kullanılarak oluşturulur. $destination
parametresi olarak {signal}!
dizesini ve $args
olarak sinyale iletmek istediğimiz argümanlar dizisini iletiriz. Sinyal her zaman mevcut presenter ve eylem üzerinde mevcut
parametrelerle çağrılır, sinyal parametreleri yalnızca eklenir. Ek olarak, en başta sinyali belirten ?do
parametresi eklenir.
Formatı ya {signal}
ya da {signalReceiver}-{signal}
şeklindedir. {signalReceiver}
,
presenter'daki bileşenin adıdır. Bu nedenle, bileşen adında tire olamaz – bileşen adını ve sinyali ayırmak için
kullanılır, ancak bu şekilde birkaç 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ı doğrular. İkinci argümanı
atlayabiliriz – o zaman bileşenin herhangi bir sinyalin alıcısı olup olmadığını kontrol eder. İkinci parametre olarak
true
belirtilebilir ve böylece yalnızca belirtilen bileşenin değil, aynı zamanda herhangi bir alt öğesinin de
alıcı olup olmadığını doğrular.
handle{signal}
'den önceki herhangi bir aşamada, sinyali manuel olarak processSignal()
metodunu çağırarak yürütebiliriz; bu metot sinyalin işlenmesini üstlenir – sinyalin alıcısı olarak belirlenen
bileşeni alır (sinyal alıcısı belirtilmemişse, presenter'ın kendisidir) ve ona sinyali gönderir.
Örnek:
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
Böylece sinyal erken yürütülür ve tekrar çağrılmaz.