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 exempluhttp://localhost/CD-collection
)$user
este un obiect care reprezintă utilizatorul$presenter
este prezentatorul curent$control
este componenta curentă$flashes
lista de mesaje trimise prin metodaflashMessage()
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?
- este redabilă într-un șablon
- știe ce parte din el însuși să redea în timpul unei cereri AJAX (fragmente)
- are capacitatea de a stoca starea sa într-un URL (parametri persistenți)
- are capacitatea de a răspunde la acțiunile utilizatorului (semnale)
- 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.