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?

  1. şablonda oluşturulabilir
  2. AJAX isteğinde hangi bölümünün oluşturulacağını bilir (snippet'ler)
  3. durumunu URL'de saklama yeteneğine sahiptir (kalıcı parametreler)
  4. kullanıcı eylemlerine yanıt verme yeteneğine sahiptir (sinyaller)
  5. 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ü

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.

versiyon: 4.0