Admin presenter#
Obrazovka administrace: výpis v DataGridu, pod ním přidání a úprava
přes formulář. Základ dělá app/Core/Base/Admin/BasePresenter.php — tahle kapitola je
o tom, co k němu musíte dopsat vy.
Co bázový presenter zařizuje sám#
startup() proběhne v tomhle pořadí a všechno z toho dostanete zadarmo:
| Krok | Co udělá |
|---|---|
| vypnutí query cache | celý admin požadavek čte z databáze, ne z Redisu |
| kontexty | nastavení, téma, lokalizace, jazyk, překladač |
checkAuthentication() |
synchronizace JWT ↔ Nette session |
authorize() |
RBAC — výchozí požadavek je privilegium :Admin |
loadUserPrivileges() |
naplní oprávnění pro isAllowed() v šablonách a gridech |
initializePage() |
dohledá AdminPage podle dvojice presenter + akce |
Navíc: createForm() (formulář s tlačítky a návratem), překladač v šabloně,
getAdminItemsPerPage() a režim Pickeru.
Admin požadavek má cache vypnutou schválně — nezapínejte ji zpátky
Vypíná se kontext celého požadavku, ne jen admin repository: v administraci běží i frontové managery (assety, téma, uživatel) a ty jdou přes repository, která cache povoluje. Bez toho by se do formuláře mohl dostat záznam z cache — a admin formulář je čtení, po kterém následuje zápis. Uložilo by se něco jiného, než co admin viděl.
Doména překladů se v administraci NEodvozuje z namespace
Na rozdíl od Front presenteru ji musíte deklarovat ručně:
protected string $translatorDomain = 'admin.blog.presenters.tag';. Bez ní se
v šabloně nepřeloží nic a createForm() přeskočí useDomainTranslator() — texty
formuláře pak zůstanou jako holé klíče. Podrobně Překlady.
Jak vypadá hotový presenter#
Soubor presenteru drží jen data, akce a továrnu gridu. Formulář je v traitu.
final class TagPresenter extends BasePresenter
{
use TagFormTrait; // createComponentEditForm + uložení
use AdminFormHelpersTrait; // getFormLanguages() + str()
use AdminEditFormComponentsTrait; // jazykové záložky + tlačítka
protected string $translatorDomain = 'admin.blog.presenters.tag';
private ?Tag $editedTag = null;
public function __construct(
private readonly DataGridFactory $dataGridFactory,
private readonly TagManager $tagManager,
) {
parent::__construct();
}
public function actionEdit(int $id): void
{
$tag = $this->tagManager->findAll(
filters: [new EqualFilter('id', $id)],
limit: 1,
includes: ['translations'],
)->first();
$this->editedTag = $tag instanceof Tag ? $tag : null;
if ($this->editedTag === null) {
$this->error('Štítek nenalezen.');
}
}
public function renderAdd(): void
{
$this->template->pageTitle = 'form.headers.add';
$this->template->languages = $this->getFormLanguages();
$this->setView('edit'); // add i edit sdílejí jednu šablonu
}
}
| Akce | Šablona | Co dělá |
|---|---|---|
show |
Templates/<Presenter>/show.latte |
výpis, jen {control grid} |
add |
sdílí edit.latte přes setView('edit') |
prázdný formulář |
edit |
Templates/<Presenter>/edit.latte |
formulář s výchozími hodnotami |
Postup: zakládám nový admin presenter#
- Model musí být hotový dřív. Admin varianta entity, manager, service, mapper a repository — viz Konvence kódu, oddíl o entitě a manageru.
- Presenter do
app/UI/Admin/<Modul>/Presenters/XxxPresenter.php:final,extends BasePresenter,$translatorDomain, private property pro editovaný záznam, konstruktor sDataGridFactorya manažerem. - Akce:
actionEdit(int $id)načte do property a při nenalezení volá$this->error();actionAdd()property vynuluje;renderAdd()/renderEdit()nastavípageTitlea jazyky.renderAdd()končísetView('edit'). - Grid v
createComponentGrid()— zdroj dat je manager, ne dotaz. NezapomeňtesetIncludes()na to, co grid opravdu zobrazuje, asetItemsPerPage($this->getAdminItemsPerPage()). - Formulář do vlastního traitu
Presenters/Traits/Forms/XxxFormTrait.php(postup níž). - Šablony
Templates/<Presenter>/show.latteaedit.latte. - Řádek v
cms_admin_system_pagess dvojicípresenter(s úvodní dvojtečkou,:Admin:Blog:Tag) aaction, plus překlady vcms_admin_system_page_texts. - Položku menu a její vazbu na stránku
(
cms_admin_system_page_menu_item_relations). - Překladové klíče do
app/Locale/<modul>/<locale>/admin.<LOCALE>.neonpodadmin.<modul>.presenters.<presenter>— a rovnou ve všech jazycích. - RBAC klíč pro akce, které nemá vidět každý; podmíněné zobrazení v gridu přes
isAllowed(). - Prokliknout: výpis, filtr, řazení, stránkování, přidání, úprava, smazání a návrat tlačítkem „Zrušit“.
Bez řádku v cms_admin_system_pages nemá stránka titulek ani menu
initializePage() hledá stránku podle přesné dvojice presenter + akce, přičemž
sloupec presenter drží plný Nette název s úvodní dvojtečkou. Když řádek chybí,
nic nespadne — jen zůstane prázdný titulek, prázdné meta tagy a v menu se
nezvýrazní žádná položka. Vypadá to jako chyba šablony.
Hezká admin URL potřebuje překlad stránky, jinak spadne na obecnou routu
AdminRouteFactory zkouší nejdřív hezký tvar admin/<presenterHref>/<actionHref>
a bez uloženého překladu vrátí null — použije se fallback
admin/<modul>/<presenter>/<akce>. Adresa funguje, jen je ošklivá; nikde se to
nehlásí.
Postup: přidávám formulář#
Kostru dělá createForm(), vy dodáte pole a callback, který uloží.
createComponentEditForm()vFormTraitpředá docreateForm()callback, který vracíbool— úspěch, nebo ne:
protected function createComponentEditForm(): Form
{
$form = $this->createForm(fn(ArrayHash $values): bool => $this->saveTagForm($values));
$this->addTranslationFields($form);
if ($this->getHttpRequest()->isMethod('GET') && $this->getSignal() === null) {
$this->setTagFormDefaults($form);
}
return $form;
}
- Výchozí hodnoty plňte jen při běžném GET renderu. Podmínka výš je záměrná — po odeslání formuláře nebo při signálu by defaulty přepsaly to, co uživatel vyplnil.
- Callback uloží přes manager, vyhodnotí
isSuccess(), pošle flash a při úspěchu nastaví$this->savedEntityId:
private function saveTagForm(ArrayHash $values): bool
{
$tag = $this->buildTagEntity($values);
$result = $this->tagManager->save($tag);
if (!$result->isSuccess()) {
$this->flashMessage($this->translatorDomain . '.form.messages.save.error', 'danger');
return false;
}
$this->flashMessage($this->translatorDomain . '.form.messages.save.success', 'success');
$this->savedEntityId = $tag->getId();
return true;
}
- Tlačítka a redirecty už řešit nemusíte:
| Tlačítko | Chování |
|---|---|
Uložit (save) |
uloží a vrátí se na stránku, ze které jste přišli |
Použít (update) |
uloží a zůstane na editaci |
Zrušit (cancel) |
jen odkaz zpět, nic neukládá |
- Návrat „zpět“ je per formulář. Adresa předchozí stránky se při prvním GET
renderu vezme z
Referer, uloží do session pod náhodný klíč a klíč jde do skrytého polebackKey. Souběžné taby si proto návrat nepřepisují.
$savedEntityId nenastavené znamená DUPLIKÁT při druhém „Použít“
Na akci add vede update na redirect('this') — tedy zpátky na prázdný add
formulář. Druhé kliknutí na „Použít“ tam založí další nový záznam. Base to ošetřuje
přesměrováním na edit?id=…, ale jen když callback $savedEntityId vyplní.
Ukládá se JEN přes save a update
Nette pouští onSuccess pro každý validní submit. Base proto uvnitř kontroluje
jméno stisknutého tlačítka a u ostatních se vrátí bez uložení. Když si přidáte
vlastní submit (překreslení Pickeru, „přidat řádek“), nebude ukládat — a to je
správně. Kdo to nečeká, hledá, proč se data neuložila.
Postup: přidávám vícejazyčná pole#
AdminFormHelpersTraitdágetFormLanguages()astr();AdminEditFormComponentsTraitjazykové záložky a lištu tlačítek.- Jedna sada polí na jazyk, sufix
_<langId>:
foreach ($this->getFormLanguages() as $language) {
$langId = (int) $language->getId();
$active = $form->addCheckbox('active_' . $langId, 'form.fields.active.label');
$form->addText('name_' . $langId, 'form.fields.name.label')
->setNullable()
->addConditionOn($active, $form::EQUAL, true)
->setRequired($t('form.fields.name.required'));
}
- Při sestavování entity dohledejte překlad přes
getTranslation($langId)a chybějící založte i ssetParent(). includes: ['translations']patří do načtení vactionEdit()— bez toho formulář nabídne prázdno.
Povinné pole v nezobrazené jazykové záložce zablokuje odeslání
Proto je setRequired() navěšené addConditionOn($active, …) — povinné je jen
tehdy, když je ten překlad označený jako aktivní. Bez podmínky nejde formulář
odeslat a prohlížeč přitom skáče na pole, které není vidět.
Nevykreslené pole SMAŽE hodnotu
Pole, které šablona nevykreslí, se neodešle — a uložení ho zapíše jako prázdné.
Netýká se to jen addHidden(): totéž udělá n:if na poli formuláře. Data
zmizí bez jediné chybové hlášky a přijde se na to podle stížnosti uživatele.
Výchozí hodnoty editace potřebují ToOne vazby v includes
Bez nich formulář nabídne prázdný výběr — a uložení tu vazbu zruší, protože prázdno je platná hodnota.
Režim Pickeru#
Presenter umí posloužit jako obsah Pickeru: vykreslí svůj show grid
jako výběrový modál v minimálním layoutu, bez přidání, editace a hromadných akcí.
Zapíná se v továrně gridu:
if ($this->picker !== '') {
$grid->setPickerMode($this->picker)->setPickerLabelProperty('translations.name');
}
Parametry jsou persistentní, aby přežily filtrování a stránkování uvnitř modálu.
pickerExclude (a dvojice pickerExcludeSubtreeLft/Rgt u stromů) skryje „Vybrat“
u záznamů, které se vybrat nesmí — typicky aby stránka nemohla být sama sobě rodičem.
Bez setPickerLabelProperty() se popisek vezme z prvního sloupce, který není identifikátor
Někdy to vyjde, někdy ne — a když je ta property u části záznamů prázdná, uživatel
po výběru uvidí prázdné pole. Když jediná property nestačí (je potřeba složit víc
polí), použijte setPickerLabelCallback(), který dostane celou entitu. Podrobně
Picker.
Na co si dát pozor#
Přesměrování na surovou adresu flash zprávu zahodí
redirectUrl() místo redirect() znamená, že uživatel neuvidí ani potvrzení, ani
chybu — akce proběhne a vypadá, že se nic nestalo.
Flash v modálním okně potřebuje vlastní kotvu
Bez ní se vykreslí mimo modál, kde ji za otevřeným oknem nikdo nevidí.
Po změně struktury formuláře je potřeba načíst stránku dvakrát
První načtení ještě jede podle staré podoby z mezipaměti. Nezakládejte na prvním pokusu závěr, že oprava nefunguje.
Administrace nesmí používat Front traity ani Front entity
Vypadá to jako úspora — obojí přece existuje. Jenže Front entita je výsledek hydratace odpovědi pro návštěvníka: nese jen to, co Front potřebuje, a mění se podle frontu. Admin, který na ni sáhne, se rozbije při úpravě, která s administrací nemá nic společného.
Kam sáhnout#
| Chci | Kde |
|---|---|
| co dělá základ | app/Core/Base/Admin/BasePresenter.php |
| kostru formuláře a návrat zpět | tamtéž → createForm(), resolveFormBackKey() |
| jazykové záložky a tlačítka | app/Core/Traits/Admin/Presenters/AdminEditFormComponentsTrait.php |
| pomocníky formuláře | app/Core/Traits/Admin/Presenters/AdminFormHelpersTrait.php |
| RBAC v administraci | app/Core/Traits/Admin/Presenters/PrivilegeCheckTrait.php |
| načtení stránky a menu | app/Core/Traits/Admin/Presenters/PageInitTrait.php |
| hezké admin adresy | app/Core/Routers/Factories/AdminRouteFactory.php |
| hotový vzor | app/UI/Admin/Blog/Presenters/TagPresenter.php + Traits/Forms/TagFormTrait.php |
Navazující kapitoly: Práce s tabulkami · Picker · Formuláře · Překlady · Konvence kódu