Componente interactive

Componentele sunt obiecte separate reutilizabile pe care le plasăm în pagini. Ele pot fi formulare, datagrids, sondaje, de fapt, orice lucru care are sens să fie folosit în mod repetat. Vom arăta:

  • cum se utilizează componentele?
  • cum să le scriem?
  • ce sunt semnalele?

Nette are un sistem de componente încorporat. Cei mai în vârstă dintre dumneavoastră își amintesc poate ceva similar din Delphi sau ASP.NET Web Forms. React sau Vue.js este construit pe ceva foarte asemănător. Cu toate acestea, în lumea cadrelor PHP, aceasta este o caracteristică complet unică.

În același timp, componentele schimbă în mod fundamental abordarea dezvoltării aplicațiilor. Puteți compune pagini din unități pregatite în prealabil. Aveți nevoie de datagrid în administrație? Îl puteți găsi la Componette, un depozit de add-on-uri (nu doar componente) open-source pentru Nette, și pur și simplu îl puteți lipi în prezentator.

Puteți încorpora orice număr de componente în prezentator. Și puteți insera alte componente în unele componente. Se creează astfel un arbore de componente cu un prezentator ca rădăcină.

Metode de fabrică

Cum sunt plasate și ulterior utilizate componentele în prezentator? De obicei, folosind metode de fabrică.

Fabrica de componente este o modalitate elegantă de a crea componente numai atunci când sunt cu adevărat necesare (leneș / la cerere). Toată magia constă în implementarea unei metode numite createComponent<Name>(), unde <Name> este numele componentei, pe care o va crea și o va returna.

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

Deoarece toate componentele sunt create în metode separate, codul este mai curat și mai ușor de citit.

Numele componentelor încep întotdeauna cu o literă minusculă, deși sunt scrise cu majuscule în numele metodei.

Nu apelăm niciodată direct fabricile, acestea sunt apelate automat, atunci când folosim componentele pentru prima dată. Datorită acestui fapt, o componentă este creată la momentul potrivit și numai dacă este cu adevărat necesară. Dacă nu am folosi componenta (de exemplu, în cadrul unor cereri AJAX, în care returnăm doar o parte din pagină sau când anumite părți sunt stocate în memoria cache), aceasta nici măcar nu ar fi creată și am economisi performanța serverului.

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

În șablon, puteți reda o componentă folosind eticheta {control}. Astfel, nu mai este nevoie să treceți manual componentele în șablon.

<h2>Please Vote</h2>

{control poll}

Stilul Hollywood

Componentele folosesc în mod obișnuit o tehnică interesantă, pe care ne place să o numim stilul Hollywood. Cu siguranță cunoașteți clișeul pe care actorii îl aud adesea la apelurile de casting: “Nu ne sunați pe noi, vă sunăm noi pe voi”. Și despre asta este vorba.

În Nette, în loc să trebuiască să puneți în permanență întrebări (“a fost trimis formularul?”, “a fost valid?” sau “a apăsat cineva acest buton?”), îi spuneți framework-ului “când se întâmplă acest lucru, apelați această metodă” și lăsați mai departe să lucreze la ea. Dacă programați în JavaScript, sunteți familiarizat cu acest stil de programare. Scrieți funcții care sunt apelate atunci când apare un anumit eveniment. Iar motorul le trece parametrii corespunzători.

Acest lucru schimbă complet modul în care scrieți aplicații. Cu cât puteți delega mai multe sarcini către framework, cu atât mai puțină muncă aveți de făcut. Și cu atât mai puțin puteți uita.

Cum se scrie o componentă

Prin componentă ne referim de obicei la descendenții clasei Nette\Application\UI\Control. Prezentatorul Nette\Application\UI\Presenter este, de asemenea, un descendent al clasei Control.

use Nette\Application\UI\Control;

class PollControl extends Control
{
}

Redarea

Știm deja că eticheta {control componentName} este utilizată pentru a desena o componentă. Aceasta apelează de fapt metoda render() a componentei, în care ne ocupăm de redare. Avem, la fel ca în prezentator, un șablon Latte în variabila $this->template, căruia îi transmitem parametrii. Spre deosebire de utilizarea într-un prezentator, trebuie să specificăm un fișier șablon și să-l lăsăm să facă randarea:

public function render(): void
{
	// vom introduce câțiva parametri în șablon
	$this->template->param = $value;
	// și îl vom desena
	$this->template->render(__DIR__ . '/poll.latte');
}

Eticheta {control} permite trecerea parametrilor către metoda render():

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

Uneori, o componentă poate fi formată din mai multe părți pe care dorim să le redăm separat. Pentru fiecare dintre ele vom crea propria metodă de redare, iată de exemplu renderPaginator():

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

Iar în șablon o vom apela apoi folosind:

{control poll:paginator}

Pentru o mai bună înțelegere, este bine de știut cum este compilat tag-ul în cod PHP.

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

Acesta se compilează la:

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

getComponent() returnează componenta poll și apoi este apelată metoda render() sau renderPaginator(), respectiv, este apelată pe aceasta.

Dacă oriunde în partea parametrilor se folosește =>, toți parametrii vor fi înfășurați cu o matrice și vor fi trecuți ca prim argument:

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

se compilează la:

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

Redarea sub-componentei:

{control cartControl-someForm}

se compilează la:

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

Componentele, precum prezentatorii, transmit automat mai multe variabile utile șabloanelor:

  • $basePath este o cale URL absolută către directorul rădăcină (de exemplu, /CD-collection)
  • $baseUrl este o adresă URL absolută către directorul rădăcină (de exemplu http://localhost/CD-collection)
  • $user este un obiect care reprezintă utilizatorul
  • $presenter este prezentatorul curent
  • $control este componenta curentă
  • $flashes lista de mesaje trimise prin metoda flashMessage()

Semnal

Știm deja că navigarea în aplicația Nette constă în crearea de legături sau redirecționarea către perechi Presenter:action. Dar ce se întâmplă dacă dorim doar să efectuăm o acțiune pe pagina curentă? De exemplu, să schimbăm ordinea de sortare a coloanei din tabel; să ștergem un element; să schimbăm modul lumină/întuneric; să trimitem formularul; să votăm în sondaj; etc.

Acest tip de cerere se numește semnal. Și, la fel ca și acțiunile, invocă metode action<Action>() sau render<Action>(), semnalele apelează metode handle<Signal>(). În timp ce conceptul de acțiune (sau vizualizare) se referă doar la prezentatori, semnalele se aplică tuturor componentelor. Și, prin urmare, și prezentatorilor, deoarece UI\Presenter este un descendent al UI\Control.

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

Legătura care apelează semnalul este creată în mod obișnuit, adică în șablon prin atributul n:href sau tag-ul {link}, în cod prin metoda link(). Mai multe informații în capitolul Crearea de legături URL.

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

Semnalul este întotdeauna apelat în prezentatorul și vizualizarea curente, astfel încât nu este posibilă crearea unei legături către semnal în alt prezentator/acțiune.

Astfel, semnalul determină reîncărcarea paginii exact în același mod ca în cererea inițială, doar că, în plus, apelează metoda de tratare a semnalului cu parametrii corespunzători. În cazul în care metoda nu există, se aruncă excepția Nette\Application\UI\BadSignalException, care este afișată utilizatorului sub forma paginii de eroare 403 Forbidden.

Fragmente și AJAX

Semnalele vă pot aminti puțin de AJAX: gestionari care sunt apelați în pagina curentă. Și aveți dreptate, semnalele sunt foarte des apelate folosind AJAX, iar apoi transmitem doar părțile modificate ale paginii către browser. Acestea se numesc snippets. Mai multe informații pot fi găsite pe pagina despre AJAX.

Mesaje Flash

O componentă dispune de propria sa memorie de mesaje flash, independent de prezentator. Acestea sunt mesaje care, de exemplu, informează cu privire la rezultatul operațiunii. O caracteristică importantă a mesajelor flash este că acestea sunt disponibile în șablon chiar și după redirecționare. Chiar și după ce au fost afișate, ele vor rămâne în viață încă 30 de secunde – de exemplu, în cazul în care utilizatorul ar reîmprospăta involuntar pagina – mesajul nu se va pierde.

Trimiterea se face prin metoda flashMessage. Primul parametru este textul mesajului sau obiectul stdClass care reprezintă mesajul. Al doilea parametru opțional este tipul acestuia (error, warning, info, etc.). Metoda flashMessage() returnează o instanță a mesajului flash ca obiect stdClass căruia i se pot transmite informații.

$this->flashMessage('Item was deleted.');
$this->redirect(/* ... */); // și redirecționarea

În șablon, aceste mesaje sunt disponibile în variabila $flashes ca obiecte stdClass, care conțin proprietățile message (text mesaj), type (tip mesaj) și pot conține informațiile deja menționate despre utilizator. Le desenăm după cum urmează:

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

Redirecționarea după un semnal

După procesarea unui semnal de componentă, urmează adesea redirecționarea. Această situație este similară formularelor – după trimiterea unui formular, redirecționăm, de asemenea, pentru a preveni retrimiterea datelor atunci când pagina este reîmprospătată în browser.

$this->redirect('this') // redirects to the current presenter and action

Deoarece o componentă este un element reutilizabil și de obicei nu ar trebui să aibă o dependență directă de anumiți prezentatori, metodele redirect() și link() interpretează automat parametrul ca fiind un semnal de componentă:

$this->redirect('click') // redirects to the 'click' signal of the same component

Dacă trebuie să redirecționați către un alt prezentator sau acțiune, puteți face acest lucru prin intermediul prezentatorului:

$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action

Parametrii persistenți

Parametrii persistenți sunt utilizați pentru a menține starea componentelor între diferite cereri. Valoarea lor rămâne aceeași chiar și după ce se face clic pe un link. Spre deosebire de datele de sesiune, aceștia sunt transferați în URL. Și sunt transferați automat, inclusiv legăturile create în alte componente din aceeași pagină.

De exemplu, aveți o componentă de paginare a conținutului. Pot exista mai multe astfel de componente pe o pagină. Și doriți ca toate componentele să rămână pe pagina lor curentă atunci când faceți clic pe link. Prin urmare, facem din numărul paginii (page) un parametru persistent.

Crearea unui parametru persistent este extrem de ușoară în Nette. Trebuie doar să creați o proprietate publică și să o marcați cu atributul: (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 publice
}

Vă recomandăm să includeți tipul de date (de exemplu, int) cu proprietatea și puteți include și o valoare implicită. Valorile parametrilor pot fi validate.

Puteți modifica valoarea unui parametru persistent atunci când creați o legătură:

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

Sau poate fi restat, adică eliminat din URL. În acest caz, va lua valoarea implicită:

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

Componente persistente

Nu numai parametrii, ci și componentele pot fi persistente. Parametrii persistenți ai acestora sunt, de asemenea, transferați între diferite acțiuni sau între diferiți prezentatori. Marcăm componentele persistente cu această adnotare pentru clasa prezentator. De exemplu, aici marcăm componentele calendar și poll după cum urmează:

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

Nu trebuie să marcați subcomponentele ca fiind persistente, acestea sunt persistente în mod automat.

În PHP 8, puteți utiliza, de asemenea, 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ă creați componente cu dependențe fără a “încurca” prezentatorii care le vor folosi? Datorită caracteristicilor inteligente ale containerului DI din Nette, ca și în cazul utilizării serviciilor tradiționale, putem lăsa cea mai mare parte a muncii în seama cadrului.

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

class PollControl extends Control
{
	public function __construct(
		private int $id, // Id-ul unui sondaj, pentru care este creată componenta
		private PollFacade $facade,
	) {
	}

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

Dacă am scrie un serviciu clasic, nu ar fi nimic de care să ne facem griji. Containerul DI s-ar ocupa în mod invizibil de trecerea tuturor dependențelor. Dar, de obicei, ne ocupăm de componente prin crearea unei noi instanțe a acestora direct în prezentator, în metodele factory createComponent...(). Dar transmiterea tuturor dependențelor tuturor componentelor către prezentator, pentru ca apoi să le transmiteți componentelor, este greoaie. Iar cantitatea de cod scrisă…

Întrebarea logică este: de ce să nu înregistrăm pur și simplu componenta ca serviciu clasic, să o transmitem prezentatorului și apoi să o returnăm în metoda createComponent...()? Dar această abordare este nepotrivită, deoarece dorim să putem crea componenta de mai multe ori.

Soluția corectă este să scriem o fabrică pentru componentă, adică o clasă care să creeze componenta pentru noi:

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

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

Acum înregistrăm serviciul nostru în containerul DI pentru configurare:

services:
	- PollControlFactory

În cele din urmă, vom utiliza această fabrică în prezentatorul nostru:

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

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

Lucrul minunat este că Nette DI poate genera astfel de fabrici simple, astfel încât, în loc să scrieți întregul cod, trebuie doar să scrieți interfața sa:

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

Asta e tot. Nette implementează intern această interfață și o injectează în prezentatorul nostru, unde o putem folosi. De asemenea, trece în mod magic parametrul $id și instanța clasei PollFacade în componenta noastră.

Componente în profunzime

Componentele într-o aplicație Nette sunt părțile reutilizabile ale unei aplicații web pe care le încorporăm în pagini, care reprezintă subiectul acestui capitol. Care sunt mai exact capacitățile unei astfel de componente?

  1. este redabilă într-un șablon
  2. știe ce parte din el însuși să redea în timpul unei cereri AJAX (fragmente)
  3. are capacitatea de a stoca starea sa într-un URL (parametri persistenți)
  4. are capacitatea de a răspunde la acțiunile utilizatorului (semnale)
  5. creează o structură ierarhică (în care rădăcina este prezentatorul)

Fiecare dintre aceste funcții este gestionată de una dintre clasele din linia de moștenire. Redarea (1 + 2) este gestionată de Nette\Application\UI\Control, încorporarea în ciclul de viață (3, 4) de către clasa Nette\Application\UI\Component, iar crearea structurii ierarhice (5) de către clasele 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 de la URL-uri sunt scrise în proprietăți prin metoda loadState(). Aceasta verifică, de asemenea, dacă tipul de date specificat pentru proprietate se potrivește, în caz contrar se va răspunde cu o eroare 404 și pagina nu va fi afișată.

Nu vă încredeți niciodată orbește în parametrii persistenți, deoarece aceștia pot fi ușor suprascriși de către utilizator în URL. De exemplu, acesta este modul în care verificăm dacă numărul paginii $this->page este mai mare decât 0. O modalitate bună de a face acest lucru este de a suprascrie metoda loadState() menționată mai sus:

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

	public function loadState(array $params): void
	{
		parent::loadState($params); // aici este setat $this->page
		// urmează verificarea valorii utilizatorului:
		if ($this->page < 1) {
			$this->error();
		}
	}
}

Procesul opus, și anume colectarea valorilor din proprietățile persistente, este gestionat de metoda saveState().

Semnale în profunzime

Un semnal determină o reîncărcare a paginii ca și cererea inițială (cu excepția AJAX) și invocă metoda signalReceived($signal) a cărei implementare implicită în clasa Nette\Application\UI\Component încearcă să apeleze o metodă compusă din cuvintele handle{Signal}. Prelucrarea ulterioară se bazează pe obiectul dat. Obiectele care sunt descendente ale Component (și anume Control și Presenter) încearcă să apeleze handle{Signal} cu parametrii relevanți.

Cu alte cuvinte: se ia definiția metodei handle{Signal} și toți parametrii care au fost primiți în cerere sunt potriviți cu parametrii metodei. Aceasta înseamnă că parametrul id din URL se potrivește cu parametrul metodei $id, something cu $something și așa mai departe. Iar dacă metoda nu există, metoda signalReceived aruncă o excepție.

Semnalul poate fi recepționat de orice componentă, prezentator al unui obiect care implementează interfața SignalReceiver, dacă este conectat la arborele de componente.

Principalii receptori de semnale sunt Presenters și componentele vizuale care extind Control. Un semnal este un semn pentru un obiect că trebuie să facă ceva – sondajul contează un vot din partea utilizatorului, caseta cu noutăți trebuie să se desfășoare, formularul a fost trimis și trebuie să proceseze datele și așa mai departe.

URL-ul pentru semnal este creat cu ajutorul metodei Component::link(). Ca parametru $destination transmitem șirul de caractere {signal}!, iar ca $args o matrice de argumente pe care dorim să le transmitem gestionarului de semnal. Parametrii semnalului sunt atașați la URL-ul prezentatorului/vederea curentă. Parametrul ?do din URL determină semnalul apelat.

Formatul său este {signal} sau {signalReceiver}-{signal}. {signalReceiver} este numele componentei din prezentator. Acesta este motivul pentru care cratima (inexact liniuță) nu poate fi prezentă în numele componentelor – este utilizată pentru a diviza numele componentei și al semnalului, dar este posibilă compunerea mai multor componente.

Metoda isSignalReceiver() verifică dacă o componentă (primul argument) este un receptor al unui semnal (al doilea argument). Al doilea argument poate fi omis – atunci se află dacă componenta este un receptor al oricărui semnal. În cazul în care al doilea parametru este true, se verifică dacă componenta sau descendenții săi sunt receptorii unui semnal.

În orice fază care precede handle{Signal} poate fi semnalul executat manual prin apelarea metodei processSignal() care își asumă responsabilitatea pentru executarea semnalului. Preia componenta receptoare (dacă nu este setată este chiar prezentatorul) și îi trimite semnalul.

Exemplu:

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

Semnalul este executat prematur și nu va mai fi apelat din nou.

versiune: 4.0