Ponovna uporaba obrazcev na več mestih
V Nette imate na voljo več možnosti, kako uporabiti isti obrazec na več mestih in ne podvajati kode. V tem članku si bomo ogledali različne rešitve, vključno s tistimi, ki se jim morate izogibati.
Tovarna obrazcev
Eden od osnovnih pristopov k uporabi iste komponente na več mestih je ustvarjanje metode ali razreda, ki to komponento generira, in nato klicanje te metode na različnih mestih aplikacije. Takšni metodi ali razredu pravimo tovarna. Prosimo, ne zamenjujte z oblikovalskim vzorcem factory method, ki opisuje specifičen način uporabe tovarn in ni povezan s to temo.
Kot primer bomo ustvarili tovarno, ki bo sestavljala urejevalni obrazec:
use Nette\Application\UI\Form;
class FormFactory
{
public function createEditForm(): Form
{
$form = new Form;
$form->addText('title', 'Naslov:');
// tukaj se dodajajo dodatna polja obrazca
$form->addSubmit('send', 'Pošlji');
return $form;
}
}
Zdaj lahko to tovarno uporabite na različnih mestih v vaši aplikaciji, na primer v presenterjih ali komponentah. In sicer tako, da jo zahtevamo kot odvisnost. Najprej torej razred zapišemo v konfiguracijsko datoteko:
services:
- FormFactory
Nato jo uporabimo v presenterju:
class MyPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FormFactory $formFactory,
) {
}
protected function createComponentEditForm(): Form
{
$form = $this->formFactory->createEditForm();
$form->onSuccess[] = function () {
// obdelava poslanih podatkov
};
return $form;
}
}
Tovarno obrazcev lahko razširite z dodatnimi metodami za ustvarjanje drugih vrst obrazcev glede na potrebe vaše aplikacije. In seveda lahko dodamo tudi metodo, ki ustvari osnovni obrazec brez elementov, in to bodo uporabljale druge metode:
class FormFactory
{
public function createForm(): Form
{
$form = new Form;
return $form;
}
public function createEditForm(): Form
{
$form = $this->createForm();
$form->addText('title', 'Naslov:');
// tukaj se dodajajo dodatna polja obrazca
$form->addSubmit('send', 'Pošlji');
return $form;
}
}
Metoda createForm()
zaenkrat ne počne ničesar uporabnega, vendar se bo to hitro spremenilo.
Odvisnosti tovarne
Sčasoma se bo izkazalo, da potrebujemo, da so obrazci večjezični. To pomeni, da moramo vsem obrazcem nastaviti t.i. prevajalnik. V ta namen bomo prilagodili razred
FormFactory
, da bo sprejemal objekt Translator
kot odvisnost v konstruktorju, in ga posredovali
obrazcu:
use Nette\Localization\Translator;
class FormFactory
{
public function __construct(
private Translator $translator,
) {
}
public function createForm(): Form
{
$form = new Form;
$form->setTranslator($this->translator);
return $form;
}
// ...
}
Ker metodo createForm()
kličejo tudi druge metode, ki ustvarjajo specifične obrazce, je dovolj, da prevajalnik
nastavimo samo v njej. In končali smo. Ni treba spreminjati kode nobenega presenterja ali komponente, kar je odlično.
Več tovarniških razredov
Alternativno lahko ustvarite več razredov za vsak obrazec, ki ga želite uporabiti v svoji aplikaciji. Ta pristop lahko
poveča berljivost kode in olajša upravljanje obrazcev. Prvotno FormFactory
bomo pustili, da ustvarja samo čist
obrazec z osnovno konfiguracijo (na primer s podporo za prevode), za urejevalni obrazec pa bomo ustvarili novo tovarno
EditFormFactory
.
class FormFactory
{
public function __construct(
private Translator $translator,
) {
}
public function create(): Form
{
$form = new Form;
$form->setTranslator($this->translator);
return $form;
}
}
// ✅ uporaba kompozicije
class EditFormFactory
{
public function __construct(
private FormFactory $formFactory,
) {
}
public function create(): Form
{
$form = $this->formFactory->create();
// tukaj se dodajajo dodatna polja obrazca
$form->addSubmit('send', 'Pošlji');
return $form;
}
}
Zelo pomembno je, da je povezava med razredoma FormFactory
in EditFormFactory
realizirana s kompozicijo, ne pa z objektnim dedovanjem:
// ⛔ TAKOLE NE! SEM DEDOVANJE NE SPADA
class EditFormFactory extends FormFactory
{
public function create(): Form
{
$form = parent::create();
$form->addText('title', 'Naslov:');
// tukaj se dodajajo dodatna polja obrazca
$form->addSubmit('send', 'Pošlji');
return $form;
}
}
Uporaba dedovanja bi bila v tem primeru popolnoma kontraproduktivna. Na težave bi naleteli zelo hitro. Na primer v trenutku,
ko bi želeli metodi create()
dodati parametre; PHP bi javil napako, da se njena signatura razlikuje od starševske.
Ali pri posredovanju odvisnosti v razred EditFormFactory
prek konstruktorja. Nastala bi situacija, ki ji pravimo constructor hell.
Na splošno je bolje dati prednost kompoziciji pred dedovanjem.
Obdelava obrazca
Obdelava obrazca, ki se pokliče po uspešnem pošiljanju, je lahko tudi del tovarniškega razreda. Delovala bo tako, da bo
poslana podatke posredovala modelu v obdelavo. Morebitne napake posreduje nazaj v obrazec. Model v naslednjem
primeru predstavlja razred Facade
:
class EditFormFactory
{
public function __construct(
private FormFactory $formFactory,
private Facade $facade,
) {
}
public function create(): Form
{
$form = $this->formFactory->create();
$form->addText('title', 'Naslov:');
// tukaj se dodajajo dodatna polja obrazca
$form->addSubmit('send', 'Pošlji');
$form->onSuccess[] = [$this, 'processForm'];
return $form;
}
public function processForm(Form $form, array $data): void
{
try {
// obdelava poslanih podatkov
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
}
}
}
Samo preusmeritev pa bomo prepustili presenterju. Ta bo dogodku onSuccess
dodal še en handler, ki bo izvedel
preusmeritev. Zaradi tega bo mogoče obrazec uporabiti v različnih presenterjih in v vsakem preusmeriti drugam.
class MyPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private EditFormFactory $formFactory,
) {
}
protected function createComponentEditForm(): Form
{
$form = $this->formFactory->create();
$form->onSuccess[] = function () {
$this->flashMessage('Zapis je bil shranjen');
$this->redirect('Homepage:');
};
return $form;
}
}
Ta rešitev izkorišča lastnost obrazcev, da ko se nad obrazcem ali njegovim elementom pokliče addError()
, se
naslednji handler onSuccess
ne pokliče več.
Dedovanje od razreda Form
Sestavljen obrazec ne sme biti potomec obrazca. Z drugimi besedami, ne uporabljajte te rešitve:
// ⛔ TAKOLE NE! SEM DEDOVANJE NE SPADA
class EditForm extends Form
{
public function __construct(Translator $translator)
{
parent::__construct();
$this->addText('title', 'Naslov:');
// tukaj se dodajajo dodatna polja obrazca
$this->addSubmit('send', 'Pošlji');
$this->setTranslator($translator);
}
}
Namesto sestavljanja obrazca v konstruktorju uporabite tovarno.
Treba se je zavedati, da je razred Form
v prvi vrsti orodje za sestavljanje obrazca, torej form builder.
In sestavljen obrazec lahko razumemo kot njen produkt. Vendar produkt ni specifičen primer graditelja (builder), med njimi ni
povezave is a, ki tvori osnovo dedovanja.
Komponenta z obrazcem
Popolnoma drugačen pristop predstavlja ustvarjanje komponente, katere del je obrazec. To daje nove možnosti, na primer izrisovanje obrazca na specifičen način, saj je del komponente tudi predloga. Ali pa je mogoče uporabiti signale za AJAX komunikacijo in nalaganje informacij v obrazec, na primer za predlaganje itd.
use Nette\Application\UI\Form;
class EditControl extends Nette\Application\UI\Control
{
public array $onSave = [];
public function __construct(
private Facade $facade,
) {
}
protected function createComponentForm(): Form
{
$form = new Form;
$form->addText('title', 'Naslov:');
// tukaj se dodajajo dodatna polja obrazca
$form->addSubmit('send', 'Pošlji');
$form->onSuccess[] = [$this, 'processForm'];
return $form;
}
public function processForm(Form $form, array $data): void
{
try {
// obdelava poslanih podatkov
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
return;
}
// sprožitev dogodka
$this->onSave($this, $data);
}
}
Ustvarili bomo še tovarno, ki bo izdelovala to komponento. Dovolj je zapisati njen vmesnik:
interface EditControlFactory
{
function create(): EditControl;
}
In dodati v konfiguracijsko datoteko:
services:
- EditControlFactory
In zdaj lahko že zahtevamo tovarno in jo uporabimo v presenterju:
class MyPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private EditControlFactory $controlFactory,
) {
}
protected function createComponentEditForm(): EditControl
{
$control = $this->controlFactory->create();
$control->onSave[] = function (EditControl $control, $data) {
$this->redirect('this');
// ali preusmerimo na rezultat urejanja, npr.:
// $this->redirect('detail', ['id' => $data->id]);
};
return $control;
}
}