Componente interactive

Componentele sunt obiecte separate, reutilizabile, pe care le inserăm în pagini. Acestea pot fi formulare, datagrid-uri, sondaje, de fapt, orice are sens să fie folosit în mod repetat. Vom arăta:

  • cum se utilizează componentele?
  • cum se scriu?
  • ce sunt semnalele?

Nette are încorporat un sistem de componente. Ceva similar ar putea fi cunoscut de veterani din Delphi sau ASP.NET Web Forms, ceva asemănător stă la baza React sau Vue.js. Cu toate acestea, în lumea framework-urilor PHP, este o caracteristică unică.

Totuși, componentele influențează în mod fundamental abordarea creării aplicațiilor. Puteți compune paginile din unități pre-pregătite. Aveți nevoie de un datagrid în administrare? Îl găsiți pe Componette, un depozit de add-on-uri open-source (adică nu doar componente) pentru Nette și îl inserați pur și simplu în presenter.

Puteți încorpora orice număr de componente într-un presenter. Și în unele componente puteți insera alte componente. Astfel se creează un arbore de componente, a cărui rădăcină este presenterul.

Metode factory

Cum se inserează componentele în presenter și cum se utilizează ulterior? De obicei, prin metode factory.

O fabrică de componente reprezintă o modalitate elegantă de a crea componente doar în momentul în care sunt cu adevărat necesare (lazy / on demand). Întreaga magie constă în implementarea unei metode cu numele createComponent<Name>(), unde <Name> este numele componentei create, și care creează și returnează componenta.

class DefaultPresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentPoll(): PollControl
	{
		$poll = new PollControl;
		$poll->items = $this->item;
		return $poll;
	}
}

Datorită faptului că toate componentele sunt create în metode separate, codul devine mai clar.

Numele componentelor încep întotdeauna cu literă mică, chiar dacă în numele metodei se scriu cu literă mare.

Fabricile nu le apelăm niciodată direct, ele se apelează singure în momentul în care folosim componenta pentru prima dată. Datorită acestui fapt, componenta este creată la momentul potrivit și doar în cazul în care este cu adevărat necesară. Dacă nu folosim componenta (de exemplu, într-o cerere AJAX, când se transferă doar o parte a paginii, sau la cache-uirea șablonului), aceasta nu se creează deloc și economisim performanța serverului.

// accesăm componenta și dacă a fost prima dată,
// se apelează createComponentPoll() care o creează
$poll = $this->getComponent('poll');
// sintaxă alternativă: $poll = $this['poll'];

În șablon, este posibil să redăm componenta folosind tag-ul {control}. Prin urmare, nu este necesar să transmitem manual componentele către șablon.

<h2>Votați</h2>

{control poll}

Stilul Hollywood

Componentele folosesc în mod obișnuit o tehnică proaspătă, pe care ne place să o numim stilul Hollywood. Cu siguranță cunoașteți celebra frază pe care participanții la castingurile de film o aud atât de des: „Nu ne sunați, vă vom suna noi”. Și exact despre asta este vorba.

În Nette, în loc să trebuiască să întrebați constant („a fost trimis formularul?”, „a fost valid?” sau „a apăsat utilizatorul acest buton?”), spuneți framework-ului „când se întâmplă asta, apelează această metodă” și lăsați restul muncii pe seama lui. Dacă programați în JavaScript, acest stil de programare vă este familiar. Scrieți funcții care sunt apelate atunci când apare un anumit eveniment. Și limbajul le transmite parametrii corespunzători.

Acest lucru schimbă complet perspectiva asupra scrierii aplicațiilor. Cu cât puteți lăsa mai multe sarcini pe seama framework-ului, cu atât aveți mai puțină muncă. Și cu atât mai puțin puteți omite.

Scriem o componentă

Prin termenul componentă înțelegem de obicei un descendent al clasei Nette\Application\UI\Control. (Mai precis ar fi, așadar, să folosim termenul „controls”, dar „controale” are un sens complet diferit în română și s-a impus mai degrabă „componente”.) Presenterul însuși Nette\Application\UI\Presenter este, de altfel, tot un descendent al clasei Control.

use Nette\Application\UI\Control;

class PollControl extends Control
{
}

Redare

Știm deja că pentru redarea unei componente se folosește tag-ul {control componentName}. Acesta apelează de fapt metoda render() a componentei, în care ne ocupăm de redare. Avem la dispoziție, la fel ca în presenter, șablonul Latte în variabila $this->template, căreia îi transmitem parametri. Spre deosebire de presenter, trebuie să specificăm fișierul cu șablonul și să îl lăsăm să fie redat:

public function render(): void
{
	// inserăm în șablon câțiva parametri
	$this->template->param = $value;
	// și o redăm
	$this->template->render(__DIR__ . '/poll.latte');
}

Tag-ul {control} permite transmiterea parametrilor către metoda render():

{control poll $id, $message}
public function render(int $id, string $message): void
{
	// ...
}

Uneori, o componentă poate consta din mai multe părți pe care dorim să le redăm separat. Pentru fiecare dintre ele, creăm propria metodă de redare, aici în exemplu, de exemplu, renderPaginator():

public function renderPaginator(): void
{
	// ...
}

Și în șablon o apelăm apoi folosind:

{control poll:paginator}

Pentru o mai bună înțelegere, este bine să știm cum se traduce acest tag în PHP.

{control poll}
{control poll:paginator 123, 'hello'}

se traduce ca:

$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');

Metoda getComponent() returnează componenta poll și pe această componentă apelează metoda render(), respectiv renderPaginator() dacă este specificat un alt mod de redare în tag după două puncte.

Atenție, dacă oriunde în parametri apare =>, toți parametrii vor fi împachetați într-un array și transmiși ca prim argument:

{control poll, id: 123, message: 'hello'}

se traduce ca:

$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);

Redarea sub-componentei:

{control cartControl-someForm}

se traduce ca:

$control->getComponent("cartControl-someForm")->render();

Componentele, la fel ca presenterele, transmit automat către șabloane câteva variabile utile:

  • $basePath este calea URL absolută către directorul rădăcină (de ex. /eshop)
  • $baseUrl este URL-ul absolut către directorul rădăcină (de ex. http://localhost/eshop)
  • $user este obiectul reprezentând utilizatorul
  • $presenter este presenterul curent
  • $control este componenta curentă
  • $flashes array de mesaje trimise de funcția flashMessage()

Semnal

Știm deja că navigarea într-o aplicație Nette constă în legarea sau redirecționarea către perechi Presenter:action. Dar ce se întâmplă dacă vrem doar să executăm o acțiune pe pagina curentă? De exemplu, să schimbăm ordonarea coloanelor într-un tabel; să ștergem un element; să comutăm între modul luminos/întunecat; să trimitem un formular; să votăm într-un sondaj; etc.

Acest tip de cereri se numește semnale. Și la fel cum acțiunile apelează metodele action<Action>() sau render<Action>(), semnalele apelează metodele handle<Signal>(). În timp ce conceptul de acțiune (sau view) este legat strict doar de presentere, semnalele se referă la toate componentele. Și, prin urmare, și la presentere, deoarece UI\Presenter este un descendent al UI\Control.

public function handleClick(int $x, int $y): void
{
	// ... procesarea semnalului ...
}

Un link care apelează un semnal se creează în mod obișnuit, adică în șablon prin atributul n:href sau tag-ul {link}, în cod prin metoda link(). Mai multe în capitolul Crearea linkurilor URL.

<a n:href="click! $x, $y">click here</a>

Semnalul se apelează întotdeauna pe presenterul și acțiunea curentă, nu este posibil să-l apelezi pe alt presenter sau altă acțiune.

Semnalul provoacă, așadar, reîncărcarea paginii la fel ca la cererea inițială, doar că în plus apelează metoda de gestionare a semnalului cu parametrii corespunzători. Dacă metoda nu există, se aruncă o excepție Nette\Application\UI\BadSignalException, care este afișată utilizatorului ca o pagină de eroare 403 Forbidden.

Snippets și AJAX

Semnalele vă pot aminti puțin de AJAX: handlere care sunt apelate pe pagina curentă. Și aveți dreptate, semnalele sunt într-adevăr adesea apelate folosind AJAX și ulterior transmitem către browser doar părțile modificate ale paginii. Adică așa-numitele snippets. Mai multe informații găsiți pe pagina dedicată AJAX.

Mesaje flash

Componenta are propriul său spațiu de stocare pentru mesaje flash, independent de presenter. Acestea sunt mesaje care, de exemplu, informează despre rezultatul unei operațiuni. O caracteristică importantă a mesajelor flash este că sunt disponibile în șablon chiar și după redirecționare. Chiar și după afișare, rămân active încă 30 de secunde – de exemplu, în cazul în care utilizatorul ar reîncărca pagina din cauza unei erori de transmisie – mesajul nu dispare imediat.

Trimiterea este gestionată de metoda flashMessage. Primul parametru este textul mesajului sau un obiect stdClass reprezentând mesajul. Al doilea parametru opțional este tipul său (error, warning, info etc.). Metoda flashMessage() returnează instanța mesajului flash ca obiect stdClass, căruia i se pot adăuga informații suplimentare.

$this->flashMessage('Elementul a fost șters.');
$this->redirect(/* ... */); // și redirecționăm

În șablon, aceste mesaje sunt disponibile în variabila $flashes ca obiecte stdClass, care conțin proprietățile message (textul mesajului), type (tipul mesajului) și pot conține informațiile utilizatorului menționate anterior. Le redăm, de exemplu, astfel:

{foreach $flashes as $flash}
	<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}

Redirecționare după semnal

După procesarea semnalului componentei, urmează adesea o redirecționare. Este o situație similară cu formularele – după trimiterea lor, redirecționăm, de asemenea, pentru ca la reîncărcarea paginii în browser să nu se trimită din nou datele.

$this->redirect('this') // redirecționează către presenterul și acțiunea curentă

Deoarece componenta este un element reutilizabil și, de obicei, nu ar trebui să aibă o legătură directă cu presentere specifice, metodele redirect() și link() interpretează automat parametrul ca un semnal al componentei:

$this->redirect('click') // redirecționează către semnalul 'click' al aceleiași componente

Dacă aveți nevoie să redirecționați către un alt presenter sau acțiune, puteți face acest lucru prin intermediul presenterului:

$this->getPresenter()->redirect('Product:show'); // redirecționează către alt presenter/acțiune

Parametri persistenți

Parametrii persistenți sunt utilizați pentru a menține starea în componente între diferite cereri. Valoarea lor rămâne aceeași chiar și după ce se face clic pe un link. Spre deosebire de datele din sesiune, acestea sunt transmise în URL. Și acest lucru se întâmplă complet automat, inclusiv pentru linkurile create în alte componente de pe aceeași pagină.

Aveți, de exemplu, o componentă pentru paginarea conținutului. Pot exista mai multe astfel de componente pe o pagină. Și dorim ca, după ce se face clic pe un link, toate componentele să rămână pe pagina lor curentă. De aceea, facem din numărul paginii (page) un parametru persistent.

Crearea unui parametru persistent în Nette este extrem de simplă. Este suficient să creați o proprietate publică și să o marcați cu un atribut: (anterior se folosea /** @persistent */)

use Nette\Application\Attributes\Persistent;  // această linie este importantă

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1; // trebuie să fie publică
}

Pentru proprietate, recomandăm să specificați și tipul de date (de ex. int) și puteți specifica și o valoare implicită. Valorile parametrilor pot fi validate.

La crearea unui link, valoarea parametrului persistent poate fi modificată:

<a n:href="this page: $page + 1">next</a>

Sau poate fi resetat, adică eliminat din URL. Atunci va lua valoarea sa implicită:

<a n:href="this page: null">reset</a>

Componente persistente

Nu doar parametrii, ci și componentele pot fi persistente. La o astfel de componentă, parametrii săi persistenți sunt transmiși și între diferite acțiuni ale presenterului sau între mai mulți presenteri. Componentele persistente le marcăm cu o adnotare la clasa presenterului. De exemplu, astfel marcăm componentele calendar și poll:

/**
 * @persistent(calendar, poll)
 */
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

Subcomponentele din interiorul acestor componente nu trebuie marcate, devin și ele persistente.

În PHP 8, puteți utiliza și atribute pentru a marca componentele persistente:

use Nette\Application\Attributes\Persistent;

#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

Componente cu dependențe

Cum să creăm componente cu dependențe fără a ne „murdări” presenterele care le vor utiliza? Datorită proprietăților inteligente ale containerului DI din Nette, la fel ca la utilizarea serviciilor clasice, putem lăsa majoritatea muncii pe seama framework-ului.

Să luăm ca exemplu o componentă care are o dependență de serviciul PollFacade:

class PollControl extends Control
{
	public function __construct(
		private int $id, // Id-ul sondajului pentru care creăm componenta
		private PollFacade $facade,
	) {
	}

	public function handleVote(int $voteId): void
	{
		$this->facade->vote($this->id, $voteId);
		// ...
	}
}

Dacă am scrie un serviciu clasic, nu ar fi nimic de rezolvat. Containerul DI s-ar ocupa invizibil de transmiterea tuturor dependențelor. Însă, cu componentele, de obicei procedăm astfel încât creăm noua lor instanță direct în presenter în metodele factory createComponent…(). Dar transmiterea tuturor dependențelor tuturor componentelor către presenter, pentru a le transmite apoi componentelor, este greoaie. Și cât cod scris…

Întrebarea logică este, de ce nu înregistrăm pur și simplu componenta ca un serviciu clasic, nu o transmitem către presenter și apoi în metoda createComponent…() nu o returnăm? O astfel de abordare este însă nepotrivită, deoarece dorim să avem posibilitatea de a crea componenta chiar și de mai multe ori.

Soluția corectă este să scriem pentru componentă o fabrică, adică o clasă care ne va crea componenta:

class PollControlFactory
{
	public function __construct(
		private PollFacade $facade,
	) {
	}

	public function create(int $id): PollControl
	{
		return new PollControl($id, $this->facade);
	}
}

Astfel înregistrăm fabrica în containerul nostru în configurație:

services:
	- PollControlFactory

și în final o folosim în presenterul nostru:

class PollPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private PollControlFactory $pollControlFactory,
	) {
	}

	protected function createComponentPollControl(): PollControl
	{
		$pollId = 1; // putem transmite parametrul nostru
		return $this->pollControlFactory->create($pollId);
	}
}

Minunat este că Nette DI poate genera astfel de fabrici simple, așa că în loc de întregul său cod, este suficient să scriem doar interfața sa:

interface PollControlFactory
{
	public function create(int $id): PollControl;
}

Și asta e tot. Nette implementează intern această interfață și o transmite către presenter, unde o putem deja utiliza. Magic, ne adaugă și parametrul $id și instanța clasei PollFacade în componenta noastră.

Componente în profunzime

Componentele din Nette Application reprezintă părți reutilizabile ale aplicației web, pe care le inserăm în pagini și cărora, de altfel, le este dedicat întregul acest capitol. Ce abilități exacte are o astfel de componentă?

  1. este redabilă în șablon
  2. știe ce parte a sa trebuie să redea la o cerere AJAX (snippets)
  3. are capacitatea de a-și salva starea în URL (parametri persistenți)
  4. are capacitatea de a reacționa la acțiunile utilizatorului (semnale)
  5. creează o structură ierarhică (unde rădăcina este presenterul)

Fiecare dintre aceste funcții este gestionată de una dintre clasele liniei ereditare. Redarea (1 + 2) este responsabilitatea Nette\Application\UI\Control, includerea în ciclul de viață (3, 4) a clasei Nette\Application\UI\Component și crearea structurii ierarhice (5) a claselor Container și Component.

Nette\ComponentModel\Component  { IComponent }
|
+- Nette\ComponentModel\Container  { IContainer }
	|
	+- Nette\Application\UI\Component  { SignalReceiver, StatePersistent }
		|
		+- Nette\Application\UI\Control  { Renderable }
			|
			+- Nette\Application\UI\Presenter  { IPresenter }

Ciclul de viață al componentei

Ciclul de viață al componentei

Validarea parametrilor persistenți

Valorile parametrilor persistenți primite din URL sunt scrise în proprietăți de către metoda loadState(). Aceasta verifică, de asemenea, dacă tipul de date specificat la proprietate corespunde, altfel răspunde cu eroarea 404 și pagina nu se afișează.

Nu credeți niciodată orbește în parametrii persistenți, deoarece pot fi ușor suprascriși de utilizator în URL. Astfel, de exemplu, verificăm dacă numărul paginii $this->page este mai mare decât 0. O cale potrivită este să suprascriem metoda menționată loadState():

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1;

	public function loadState(array $params): void
	{
		parent::loadState($params); // aici se setează $this->page
		// urmează controlul propriu al valorii:
		if ($this->page < 1) {
			$this->error();
		}
	}
}

Procesul invers, adică colectarea valorilor din proprietățile persistente, este responsabilitatea metodei saveState().

Semnale în profunzime

Un semnal provoacă reîncărcarea paginii exact la fel ca la cererea inițială (cu excepția cazului în care este apelat prin AJAX) și apelează metoda signalReceived($signal), a cărei implementare implicită în clasa Nette\Application\UI\Component încearcă să apeleze o metodă compusă din cuvintele handle{signal}. Procesarea ulterioară depinde de obiectul respectiv. Obiectele care moștenesc de la Component (adică Control și Presenter) reacționează încercând să apeleze metoda handle{signal} cu parametrii corespunzători.

Cu alte cuvinte: se ia definiția funcției handle{signal} și toți parametrii care au venit cu cererea, iar argumentelor li se atribuie parametrii din URL după nume și se încearcă apelarea metodei respective. De exemplu, ca parametru $id se transmite valoarea din parametrul id din URL, ca $something se transmite something din URL, etc. Și dacă metoda nu există, metoda signalReceived aruncă o excepție.

Semnalul poate fi primit de orice componentă, presenter sau obiect care implementează interfața SignalReceiver și este conectat la arborele de componente.

Principalii destinatari ai semnalelor vor fi Presenterele și componentele vizuale care moștenesc de la Control. Semnalul trebuie să servească drept semn pentru obiect că trebuie să facă ceva – sondajul trebuie să numere votul utilizatorului, blocul cu știri trebuie să se extindă și să afișeze de două ori mai multe știri, formularul a fost trimis și trebuie să proceseze datele și așa mai departe.

URL-ul pentru semnal îl creăm folosind metoda Component::link(). Ca parametru $destination transmitem șirul {signal}! și ca $args un array de argumente pe care dorim să le transmitem semnalului. Semnalul se apelează întotdeauna pe presenterul și acțiunea curentă cu parametrii curenți, parametrii semnalului se adaugă doar. În plus, se adaugă chiar la început parametrul ?do, care specifică semnalul.

Formatul său este fie {signal}, fie {signalReceiver}-{signal}. {signalReceiver} este numele componentei în presenter. De aceea, în numele componentei nu poate exista cratimă – se folosește pentru a separa numele componentei și semnalul, însă este posibil să se imbricheze astfel mai multe componente.

Metoda isSignalReceiver() verifică dacă componenta (primul argument) este destinatarul semnalului (al doilea argument). Al doilea argument poate fi omis – atunci verifică dacă componenta este destinatarul oricărui semnal. Ca al doilea parametru se poate specifica true și astfel se verifică dacă destinatarul este nu numai componenta specificată, ci și oricare dintre descendenții săi.

În orice fază anterioară handle{signal} putem executa semnalul manual apelând metoda processSignal(), care se ocupă de gestionarea semnalului – ia componenta care a fost desemnată ca destinatar al semnalului (dacă nu este specificat un destinatar al semnalului, acesta este presenterul însuși) și îi trimite semnalul.

Exemplu:

if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
	$this->processSignal();
}

Astfel, semnalul este executat prematur și nu va mai fi apelat din nou.

versiune: 4.0