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ğin http://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?

  1. Bir şablonda render edilebilir
  2. Bir AJAX isteğinde kendisinin hangi bölümünü render edeceğini bilir (snippet'ler)
  3. Durumunu URL'de saklama yeteneğine sahiptir (kalıcı parametreler)
  4. Kullanıcı eylemlerine tepki verme yeteneğine sahiptir (sinyaller)
  5. 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ü

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.

versiyon: 4.x