Lze použít vícekrát onSuccess[]?

Jan Kostohryz
Člen | 14
+
0
-

Zdravím, zajímalo by mě jesti lze v nette vívekrát použít onSucces? Jde o to že mám komponentu s formulářem a chtěl bych aby se dal obsah ve formuláři ukládat – insert a i editovat – update. Co z toho se provede by rozhodl Presenter pomocí akcí.
Že by byl odkaz v actionNew:
$this->[‚Form‘]->addSubmit(‚save‘, ‚přidat hru‘);
a v komponentě by bylo.
$form->onSuccess[‚save‘] = [$this, ‚save‘];
Pak se zavolá funkce save a provede uložení.
A pro editování by bylo místo save edit.

Polki
Člen | 553
+
0
-

V editaci většinou předvyplníš formulář výchozími hodnotami.
Takže jestli jde o editaci, nebo vložení poznáš už ve formuláři a to tak, že pokud tam pošleš výchozí data, tak je to editace, pokud ne, tak je to přidání. Nepotřebuješ 2 onSuccess metody.

Šaman
Člen | 2635
+
+3
-

Dá, provedou se postupně jak byly navěšeny. Používám to tak, že komponenta (formulář) si provede zpracování dat, ale přesměrování a flashmessage navěsí až presenter.

Michal Kumžák
Člen | 106
+
+1
-

Tohle řeším tak, že data z formuláře v success metodě pošlu do modelu a tam provedu zpracování. Pokud existuje například hodnota id, tak provedu UPDATE, jinak INSERT.

Jan Kostohryz
Člen | 14
+
0
-

Šaman napsal(a):

Dá, provedou se postupně jak byly navěšeny. Používám to tak, že komponenta (formulář) si provede zpracování dat, ale přesměrování a flashmessage navěsí až presenter.

Děkuji, ukázal byste mi prosím nějaký příklad? Abych se v tom lépe zorientoval.

Jan Kostohryz
Člen | 14
+
0
-

Michal Kumžák napsal(a):

Tohle řeším tak, že data z formuláře v success metodě pošlu do modelu a tam provedu zpracování. Pokud existuje například hodnota id, tak provedu UPDATE, jinak INSERT.

Děkuji, ukázal byste mi prosím nějaký příklad?

Michal Kumžák
Člen | 106
+
0
-

control

	/**
	 * @return UI\Form
	 */
	public function createComponentDruhyDopravy() {
		$form = new UI\Form;
		$form->setRenderer(new AlesWita\FormRenderer\BootstrapV4Renderer);

		$form->addGroup();
		$form->addText('nazev', 'Druh dopravy:')
			->setRequired('Prosím zadejte druh dopravy.');

		$form->addGroup();
		$form->addSubmit('send', 'Uložit');
		$form->addSubmit('zpet', 'Zpět')
			->setValidationScope([]);

		$form->addHidden('id');

		$form->setDefaults($this->druhyDopravyModel->getDruhDopravyById($this->id));

		$form->onSuccess[] = [$this, 'druhyDopravySucceeded'];
		return $form;
	}

	public function druhyDopravySucceeded($form, $values) {
		if ($form['send']->isSubmittedBy()) {
			$this->druhyDopravyModel->saveDruhDopravy($values);
		}
		$this->onFormSave($this);
	}

model

	public function saveDruhDopravy($values) {
		if ($values->id > 0) {
			$this->database->query("UPDATE `ciselniky_druhydopravy` SET `nazev`=? WHERE `id`=?", $values->nazev, $values->id);
		} else {
			$this->database->query("INSERT INTO `ciselniky_druhydopravy` (`nazev`) VALUES (?)", $values->nazev);
		}
	}
Polki
Člen | 553
+
0
-

FormFactory:

class FormFactory
{
	public function __construct(
		private Repository $repository,
	) { }

	public function create(int $id): Form
	{
		$form = new Form();
		$form->addHidden('id');
		$form->addText('content', 'Obsah zprávy')
			->setRequired();

		$form->setDefaults($this->repository->get($id) ?? []);
		$form->addSubmit('submit', 'Odeslat');

		$form->onSuccess[] = [$this, 'onSuccess'];
		return $form;
	}

	public function onSuccess(Form $form): void
	{
		try {
			$this->repository->save($form->getValues(true));
		} catch (Exception $e) {
			$form->addError('Vyskytla se chyba.');
		}
	}
}

Repository:

class Repository
{
	public function __construct(
		private Explorer $db,
	) { }

	public function save(array $values): void
	{
		$sel = $this->db->table('repo');
		$id = $values['id'];
		unset($values['id']);
		if ($id) {
			$sel->get($id)->update($values);
		} else {
			$sel->insert($values);
		}
	}

	public function get(int $id): ?ActiveRow
	{
		return $this->db->table('repo')
			->get($id);		// Nula v MySQL vrátí nic, jelikož se indexuje od 1
	}
}

Presenter:

class Presenter extends UiPresenter
{
	#[Inject] public FormFactory $formFactory;
	private int $id;

	public function actionAdd(): void {
		$this->id = 0;
	}

	public function actionEdit(int $id): void {
		$this->id = $id;
	}

	public function createComponentForm(): Form
	{
		$form = $this->formFactory->create($this->id);
		$form->onSuccess[] = [$this, 'onSuccess'];
		return $form;
	}

	public function onSuccess(): void
	{
		$this->flashMessage('Úspěšně uloženo', 'success');
		$this->redirect('this');
	}
}

Upozorňuju, že copypasta není dobrý nápad. Jde o příklad. Není zde ošetřeno práva k signálům a ani to není úplně všechno čistě napsané. Doporučuju projít třeba kurz, kde je to všechno ukázané i se zabezpečením.

Editoval Polki (15. 10. 2021 18:27)

m.brecher
Generous Backer | 758
+
0
-

Jeden formulář, který by uměl „VŠECHNO“ – založit nový, editovat a smazat záznam je potřeba dost často. Já to řeším jedním formulářem a jednou akcí v presenteru pro všechno.

požadované akce rozlišuji takto:

  • založit nový – parametr id je null, formulář odesílá tlačítko „Uložit“
  • editovat – parametr id je int a příslušný záznam existuje, formulář odesílá tlačítko „Uložit“
  • smazat – parametr id je int a příslušný záznam ke smazání existuje, formulář odesílá tlačítko „Smazat“
  1. Routy:

Akce edit má jeden nepovinný parametr id, který je typu int. Kontrolu typu id provádím už na úrovni Routeru, je to lepší – pokud bude id=blablabla, Router vyhodí výjimku 404, což je logicky správně, jinak by PHP vyhodilo TypeError až v předání id do akce presenteru, což by dopadlo výjimkou 500 a to je logicky nesprávně.

final class RouterFactory
{
	use Nette\StaticClass;

    public static function createRouter(): RouteList
    {
        $router = new RouteList;

       .......

        $router->addRoute('<presenter>/edit[/<id>]', [
            'presenter' => [ Route::VALUE => 'Homepage' ],
            'action' => [ Route::VALUE => 'edit' ],
            'id' => [  Route::PATTERN => '[1-9]\d*' ]    // povolí pouze 1, 2, 3, ...
            ]);

		........

        $router->addRoute('<presenter>/<action>', 'Homepage:default');

        return $router;
    }
}
  1. Presenter

Formulář pracuje v jedné akci „edit“ ve dvou módech který je určen property $update = (bool)$id (nula pro id je vyloučena v Routeru), je přehlednější v kódu odkazovat na $update než např. $id === null.

Tak jako tak musíme řešit existenci záznamu k editaci/mazání – v metodě actionEdit(), potom už máme data záznamu k dispozici a je ideální je ihned předat do editovaného formuláře.

$update předáme do šablony, kde se může hodit pro drobnou modifikaci vykreslování.

V tovární metodě formuláře provedeme drobné modifikace formuláře podle módu $update – např. přidáme tlačítko pro smazání

Protože formulář může být odeslán různými tlačítky – Uložit/Smazat, použijeme nikoliv událost onSuccess, ale onClick na jednotlivých tlačítkách a dva různé ovladače handleSave() a handleDelete(). Pozor! při mazání záznamu je vhodné vypnout validaci formuláře – to zajistí metoda ->setValidationScope([]);

Ovladač handleSave() zpracuje oba módy $update = true/false pro editaci i nový záznam. Oba ovladače handleSave()handleDelete() zachycují výjimky, které případně vyhodí model – buďto PDOExceptionNette\Database, nebo případně vlastní výjimky které si vyhodíme sami v modelu je-li potřeba App\Exceptions\AppException.

final class SectionAdminPresenter extends AdminBasePresenter
{
    private bool $update;	// mód formuláře update/insert
    private ?int $id;		// id editovaného záznamu

    public function __construct( private App\Model\SectionModel $sectionModel)
    {}

    public function actionEdit(?int $id = null)
    {
        $this->id = $id;
        $this->template->update = $this->update = (bool)$id;	// $this->update je čitelnější než testovat v kódu id

        if($this->update){
            $section = $this->sectionModel->getSectionById($id);
            if(!$section){
                $this->error('Tato sekce nebyla nalezena');   // 404 pokud záznam neexistuje
            }
			$this['sectionForm']->setDefaults($section);	// naplnění formuláře editovanými daty
            $this->template->section = $section;
        }
    }

    public function createComponentSectionForm(): Nette\Application\UI\Form
    {
        $form = new Nette\Application\UI\Form();

        ....... sestavení prvků formuláře

        $form->addSubmit('saveButton', 'Uložit!')
			->onClick[] = [$this, 'handleSave'];

        if($this->update){
            $form->addSubmit('deleteButton', 'Smazat!')
				->setValidationScope([])				// vypnutí validace při mazání
				->onClick[] = [$this, 'handleDelete'];
        }
        return $form;
    }

    public function handleSave(Nette\Utils\ArrayHash $values)
    {
        try{
            if($this->update){			// editace záznamu
                $updated = $this->sectionModel->updateSection($this->id, $values);
                $updated ? $this->flashMessage("Sekce byla úspěšně editována", 'success') :
                            $this->flashMessage("Sekce nebyla změněna");
                $this->redirect('this');

            }else{						// nový záznam
                $section = $this->sectionModel->insertSection($values);
                $this->flashMessage("Povedlo se to - vložena nová sekce '{$section->title}'", 'success');
                $this->redirect("Section:show", ['url' => $section->url]);
            }

        }catch( \PDOException $exception){
            $this->flashMessage('Něco se nepovedlo - '.$exception->getMessage(), 'error');

        }catch (App\Exceptions\AppException $exception){
            $this->flashMessage($exception->getMessage(), 'error');
        }
    }

    public function handleDelete()
    {
        try{						// mazání záznamu
            $count = $this->sectionModel->deleteSection($this->id);
            $this->flashMessage("Povedlo se to - smazáno $count sekcí", 'success');
            $this->redirect("Homepage:default");

        }catch( \PDOException $exception){
            $this->flashMessage('Něco se nepovedlo - '.$exception->getMessage(), 'error');

        }catch (App\Exceptions\AppException $exception){
            $this->flashMessage($exception->getMessage(), 'error');
        }
    }
}
  1. Model
final class SectionModel
{
    public function __construct(
        private Nette\Database\Explorer $database,
        private Nette\Security\User $user,
    ){}

  .......

    public function getSectionById(int $id): ?ActiveRow
    {
        return $this->database->table('section')->get($id);
    }

    public function insertSection(ArrayHash $values): ?ActiveRow
    {
        $values->url = Nette\Utils\Strings::webalize($values->title);  // úprava data před uložením
        $values->user_id = $this->user->getIdentity()->getId();

		..... další úpravy dat

        return $this->database->table('section')->insert($values);
    }

    public function updateSection(int $id, ArrayHash $values): bool
    {
        $values->url = Nette\Utils\Strings::webalize($values->title);  // úprava data před uložením

		..... další úpravy dat

        return $this->getSectionById($id)->update($values);
    }

    public function deleteSection(int $id): int
    {
        return $this->getSectionById($id)->delete();
    }
}

Editoval m.brecher (17. 10. 2021 16:13)

Michal Kumžák
Člen | 106
+
0
-

m.brecher napsal(a):

Jeden formulář, který by uměl „VŠECHNO“ – založit nový, editovat a smazat záznam je potřeba dost často. Já to řeším jedním formulářem a jednou akcí v presenteru pro všechno.

To není příliš šikovný přístup. Mnohem lepší je mít pro formulář samostatnou komponentu a v presenteru mít klasicky tři akce add, edit, delete.

m.brecher
Generous Backer | 758
+
0
-

Michal Kumžák napsal(a):

m.brecher napsal(a):

Jeden formulář, který by uměl „VŠECHNO“ – založit nový, editovat a smazat záznam je potřeba dost často. Já to řeším jedním formulářem a jednou akcí v presenteru pro všechno.

To není příliš šikovný přístup. Mnohem lepší je mít pro formulář samostatnou komponentu a v presenteru mít klasicky tři akce add, edit, delete.

@MichalKumžák Ahoj, můžeš prosím Tě poslat ukázku kódu formuláře v samostatné komponentě – včetně navazujícího kódu v presenteru?

Michal Kumžák
Člen | 106
+
+1
-

presenter

<?php
declare(strict_types=1);

namespace App\AdminModule\CiselnikyModule\Presenters;

use Nette,
	App\Model,
	App\Presenters;

class MenyPresenter extends Presenters\BasePresenter
{

	/** @var Model\KurzyModel */
	private $kurzyModel;

	/** @var \IMenyControlFactory @inject */
	public $menyControlFactory;

	function __construct(Model\KurzyModel $kurzyModel) {
		parent::__construct();
		$this->kurzyModel = $kurzyModel;
    }

	public function renderDefault($page='') {
		if (!$this->user->isAllowed('ciselniky', 'read')) {
			$this->error('Nemáte oprávění na tuto akci', 403);
		}
		$this->template->meny = $this->kurzyModel->getMeny();
	}

	public function renderAdd() {
		if (!$this->user->isAllowed('ciselniky', 'add')) {
			$this->error('Nemáte oprávění na tuto akci', 403);
		}
	}

	public function renderEdit($id) {
		if (!$this->user->isAllowed('ciselniky', 'update')) {
			$this->error('Nemáte oprávění na tuto akci', 403);
		}
	}

	public function actionDelete($id) {
		if (!$this->user->isAllowed('ciselniky', 'delete')) {
			$this->error('Nemáte oprávění na tuto akci', 403);
		}
		$rows = $this->kurzyModel->deleteMena($id);
		if ($rows > 0) {
			$this->flashMessage('Měna byla smazána.');
		} else {
			$this->flashMessage('Měna nejde smazat, je používána.');
		}
		$this->redirect('default');
	}

	protected function createComponentKurzy() {
		$control = $this->menyControlFactory->create();
		$control->setId($this->getParameter('id'));
		$control->onFormSave[] = function (\MenyControl $control) {
			$this->redirect('default');
		};

		return $control;
	}
}

latte pro akci add a edit může být stejné

{block content}
<h1>Nový kurz / Editace kurzu</h1>

{control kurzy}

control

<?php
declare(strict_types=1);

use Nette\Application\UI;

class KurzyControl extends UI\Control
{
	/** @var App\Model\KurzyModel */
	private $kurzyModel;

	public $onFormSave;

	private $id;

	public function __construct(App\Model\KurzyModel $kurzyModel) {
		$this->kurzyModel = $kurzyModel;
	}

	public function setId($id) {
		$this->id = $id;
	}

	public function render() {
		$this->template->setFile(__DIR__ . '/kurzy.latte');
		$this->template->id = $this->id;
		$this->template->render();
	}

	/**
	 * @return UI\Form
	 */
	public function createComponentKurzy() {
		$form = new UI\Form;
		$form->setRenderer(new AlesWita\FormRenderer\BootstrapV4Renderer);

		$form->addSelect('mena_id', 'Měna:', $this->kurzyModel->getMenyArray(false, 'CZK'))
			->setHtmlAttribute('title', 'Prosím vyberte měnu')
			->setRequired('Prosím vuberte měnu.');

		$form->addText('kurz', 'Kurz:')
			->setHtmlType('number')
			->setRequired('Prosím zadejte kurz.');

		$form->addText('platnost_od', 'Platnost od:')
			->setHtmlType('date')
			->setRequired('Prosím zadejte platnost od.');

		$form->addSubmit('send', 'Uložit');
		$form->addSubmit('zpet', 'Zpět')
			->setValidationScope([]);

		$form->addHidden('id');

		$form->setDefaults($this->kurzyModel->getKurzById($this->id));

		$form->onSuccess[] = [$this, 'kurzySucceeded'];
		return $form;
	}

	public function kurzySucceeded($form, $values) {
		if ($form['send']->isSubmittedBy()) {
			$this->kurzyModel->saveKurz($values);
		}
		$this->onFormSave($this);
	}
}

kurzy.latte

{control kurzy}

a ještě interface továrny

<?php
declare(strict_types=1);

interface IKurzyControlFactory
{
	/** @return KurzyControl */
	public function create();
}

Jo a když by nepotřeboval dělat kontrolu práv, tak renderAdd a renderEdit ani nemusí v presenteru být, stačí mít add.latte a edit.latte

Editoval Michal Kumžák (17. 10. 2021 17:55)

Polki
Člen | 553
+
0
-

Spravne je to jak pise @MichalKumžák
Ohledne zjistovani existence neceho v route bych byl opatrny muze se snadno stat, ze budes mit spousty zbytecnych dotazu do db.
Taky to, ze se to bude rozhodovat az v presenteru nema vliv na to jaky error se vrati

m.brecher
Generous Backer | 758
+
0
-

Polki napsal(a):

Spravne je to jak pise @MichalKumžák
Ohledne zjistovani existence neceho v route bych byl opatrny muze se snadno stat, ze budes mit spousty zbytecnych dotazu do db.
Taky to, ze se to bude rozhodovat az v presenteru nema vliv na to jaky error se vrati

@Polki

V Routě ověřuji jenom formát parametru id – samozřejmě bez dotazů do databáze, tomu je lepší se v Routeru vyhnout. Id musí být integer vyjma 0. Pokud ne tak Router vyhodí 404.

Pokud Router povolí jakékoliv id, třeba i „abc“, tak v presenteru, kde se předává id jako parametr do akce takto:

declare(strict_types=1);

.........

public function actionEdit(?int $id = null)
{
.....
}

dojde k vyhození TypeError, protože id = „abc“ neproleze typehintem v metodě akce. Výsledkem by byla nepěkná 500 v situaci, kdy server nemá žádnou vnitřní chybu, ale klient pouze poslal neplatné url. Ale není to moc důležité, protože:

Přímo v presenteru jde také vyhodit 404 pro id = „abc“, museli bychom ale upustit od typování předávaného parametru do akce:

declare(strict_types=1);

.........

public function actionEdit($id = null)		// nyní může být id 1,2,3 ale i abc ;)
{
.....
}

model by pro id=„abc“ vrátil null a 404 by vyhodil presenter – tak jako tak se musí existence záznamu ověřovat.

Polki
Člen | 553
+
+1
-

Tak to máš v aplikaci asi něco špatně…

Toto je hláška, kterou Nette vyhodí, pokud mám parametr ID s vynuceným datovým typem pouze ve funkci presenteru:

Nette\Application\BadRequestException #404

Argument $id passed to App\Presenters\HomepagePresenter::actionDefault() must be int, string given.

Takže je jasné, že to ověřuje správně a vyhazuje to BadRequestException, tedy 404…
Funkce v Presenteru vypadá úplně stejně jako ta tvoje a router takto:

public static function createRouter(Nette\Security\User $user, Nette\Http\Request $request): RouteList
{
	$router = new RouteList;
	$router->addRoute('<presenter>/<action>', 'Homepage:default');
	return $router;
}

EDIT 1:
Samozřejmě je dobré ověřovat v routě například rozsah. Například pokud máme ID jako AI v DB, tak je dobré ověřovat, jestli to ID není menší, než 1. Víme totiž, že pro 0 a záporné čísla záznamy neexistují. Podobně stránkování atp. Díky tomu se nemusí provádět dotaz do db a ušetří se nějaký čas.
V dnešním světě ale okolo ID se spíš řeší, jestli mají správný UUID formát, jelikož se doporučuje používat UUID, místo inkrementálního čítače.

Editoval Polki (17. 10. 2021 20:39)

Polki
Člen | 553
+
0
-

@mbrecher
BTW v tvém případě pokud budeš chtít nastavit pro akce ADD, EDIT a DELETE různá práva, tak to bude obtížné.
Navíc pokud budeš provádět DELETE, tak se ta ze startu bude chovat jako EDIT, což znamená, že provede natažení prvku z databáze, vytvoření formuláře, vyhodnocení formuláře, zavolání všech callbacků navěšených na formuláři a až nakonec nějaký SuccessCallback, který ještě bude špatně ifem rozdělený na 2 rozdílné funkčnosti (INSERT/EDIT a DELETE) – Říká ti něco princip SingleResponsibility?
A to nemluvím o situacích, kdy chceš mít tlačítko na smazání například ve výpise článků v adminu, kde se jinak formulář vůbec nepoužívá. Najednou budeš mít buď zdvojenou logiku pro mazání, nebo budeš používat formulář na něco, na co se používat nemá.

Přitom pro delete úplně stačí například toto:

public function handleDelete(int $id): void	// nebo actionDelete...
{
	if(!$this->user->isAllowed('Item', 'Delete')) {
		// Not allowed error nebo 404 pokud nechceš aby uživatelé, kteří nemají přístup nevěděli, jakou používáš URL pro mazání
	}
	if (!$this->repository->delete($id)) {
		// 404 záznam neexistuje
	}
	$this->redrawControl('ItemsTable'); // Nebo přesměrování, pokud nejsi ve výpise, ale v detailu.
}

A v repu klasicky:

public function delete(int $id): int
{
	return $this->db->table('items')		// pokud neexistuje záznam s daným ID, tak jej nesmaže a vrátí nulu, takže v presenteru se to vyhodnotí jako true a vypíše se 404 error záznam nenalezen
		->where('id', $id)
		->delete();
}

Tento způsob zápisu udělá 1 dotaz do db, jsou ošéfena práva a hlavně nebudeš zbytečně tvořit tunu instancí tříd formulářových prvků, komponent apod, které se stejně nevyužijí.

Proto je best mít to jak psal @MichalKumžák

Editoval Polki (17. 10. 2021 21:03)

m.brecher
Generous Backer | 758
+
0
-

Polki napsal(a):

Tak to máš v aplikaci asi něco špatně…

Toto je hláška, kterou Nette vyhodí, pokud mám parametr ID s vynuceným datovým typem pouze ve funkci presenteru:

Nette\Application\BadRequestException #404

Argument $id passed to App\Presenters\HomepagePresenter::actionDefault() must be int, string given.

@Polki ANO, máš pravdu, díky moc za upozornění, na tu 500 jsem u id=abc narazil, když jsem si zkoušel Nette blog v quickstartu a docela dlouho jsem to zkoumal a pořád mě to házelo 500. Takže jsem tehdy udělal závěr, že to je vlastnost frameworku. Tak jsem příjemně překvapen, že se formát parametru nemusí v Routeru vlastně vůbec zkoumat :).

m.brecher
Generous Backer | 758
+
0
-

Polki napsal(a):

@mbrecher
BTW v tvém případě pokud budeš chtít nastavit pro akce ADD, EDIT a DELETE různá práva, tak to bude obtížné.
Navíc pokud budeš provádět DELETE, tak se ta ze startu bude chovat jako EDIT, což znamená, že provede natažení prvku z databáze, vytvoření formuláře, vyhodnocení formuláře, zavolání všech callbacků navěšených na formuláři a až nakonec nějaký SuccessCallback, který ještě bude špatně ifem rozdělený na 2 rozdílné funkčnosti (INSERT/EDIT a DELETE) – Říká ti něco princip SingleResponsibility?
A to nemluvím o situacích, kdy chceš mít tlačítko na smazání například ve výpise článků v adminu, kde se jinak formulář vůbec nepoužívá. Najednou budeš mít buď zdvojenou logiku pro mazání, nebo budeš používat formulář na něco, na co se používat nemá.

@Polki máš to velmi dobře promyšlené z pohledu praxe a velmi jednoduché kódy. Nechci s Tebou polemizovat, že jak to navrhuješ to bezpochyby bude dobře fachat. Ale:

a ) dvě rozdílné funkčnosti v handleru pro INSERT/EDIT s ifem, který by neměl být.

INSERT/EDIT provádí formulář na úrovni signálu a akce presenteru je tam nepodstatná. Pokud bych chtěl oddělit obsluhu INSERT/EDIT do samostatných callbacků, tak bych to řešil dvěma různými tlačítky pro INSERT/EDIT, na které bych navěsil samostatné callbacky. Vložit jeden a ten samý formulář pro INSERT/EDIT do různých akcí – tím oddělení callbacků nevyřešíme.

b ) Chování DELETE jako EDIT

Formulář s editačním a mazacím tlačítkem je záměrně vícefunkční komponenta, protože pro uživatele je to takhle ideální koncept. Zobrazit si detail entity a mít po ruce všechny operace které se s ní dají udělat. Ale je to jak píšeš – pokud uživatel neprovede editaci, ale mazání, tak byla příprava na editaci „zbytečná“.

c ) Možná bychom to všechno mohli někdy probrat v hospodě u sklenky oroseného…

m.brecher
Generous Backer | 758
+
0
-

@MichalKumžák

Díky Michale za podrobný kód, který jsem si se zájmem prostudoval a vcelku pochopil.

  • formulář zaopatřuješ další funkcionalitou, kterou dodá komponenta
  • komponentu vytváří factory, realizovaná pomocí interface
  • factory se injektne do presenteru
  • presenter volá metodu createComponentXxx a ta zavolá factory, která komponentu dodá
  • komponenta je definovaná třídou odvozenou od UI/Control
  • komponenta obsahuje také metodu createComponentXxx, která vytváří ten formulář – to je pro mne velká novinka, že to takto jde :)

Všechno je jasné a transparentní, jenom nechápu tyhle drobnosti:

  • interface factory používá IKurzyControlFactory, ale do MenyPresenteru se injektuje IMenyControlFactory – asi překlep?
  • do MenyPresenteru injektuješ anotací @inject IMenyControlFactory, ale Model\KurzyModel předáváš konstruktorem – to má nějaký účel?

    nešlo by oboje předat konstruktorem, jak je tzv. best practice pro presentery?

V presenteru v metodě createComponentKurzy() máš definován takovýto callback:

$control->onFormSave[] = function (\MenyControl $control) {
			$this->redirect('default');
		};
  • zde nechápu použití argumentu callbacku \MenyControl $control který se v těle funkce vůbec nevyužije

Ve třídě KurzyControl je metoda createComponentKurzy(), která se jmenuje stejně jako obdobná metoda v MenyPresenter – shoda názvů má nějakou funkci, nebo je to náhoda, a jak se volá metoda createComponentKurzy(), která je v komponentě?

Chápu, že v šabloně je tag na vykreslení komponenty {control kurzy}, presenter volá metodu createComponentKurzy(), ta zavolá factory a ta vytvoří instanci třídy KurzyControl a dodá ji presenteru a presenter ji dodá šabloně k vykreslení. Šablona zavolá metodu render() komponenty a ta použije šablonu ‚/kurzy.latte‘. Úplně nechápu, jak se vytvoří formulář – kdo vlastně zavolá metodu createComponentKurzy() která je uvnitř komponenty a která udělá formulář? Předpokládám, že v šabloně komponenty ‚/kurzy.latte‘ je tag na vykreslení formuláře, šablona předá požadavek na formulář komponentě a ta zavolá (podobně jako presenter) svoji vlastní metodu createComponentKurzy()?

Jinak je to všechno celkem jasné a je to skvělý zapouzdřený znovupoužitelný kód.

Na závěr mám poznámku k nedostatečnému zabezpečení v akcích presenteru jak máš ve svém MenyPresenter – toto jsme s @Polki diskutovali celkem nedávno. Formuláře se zpracovávají technologií signálů, které obcházejí akce presenteru. To znamená, že i když v jedné akci zabezpečíš přístup ke komponentě, chytrý útočník to snadno obejde přes jinou akci – buďto veřejně přístupnou, nebo i neexistující akci. Presenter totiž zpracuje signál komponent ještě před dohledáním šablony akce , kdy se teprve přijde na to, že šablona neexistuje a tudíž akce také ne – už je ale pozdě, útok je dokonán. Nejlepší je zabezpečení dělat ve startupu presenteru, to bys ale musel akce rozdělit do různých presenterů. Nebo kontrolovat zabezpečení v modelu.

Polki
Člen | 553
+
0
-

@mbrecher

a ) dvě rozdílné funkčnosti v handleru pro INSERT/EDIT s ifem, který by neměl být.

INSERT/EDIT provádí formulář na úrovni signálu a akce presenteru je tam nepodstatná. Pokud bych chtěl oddělit obsluhu INSERT/EDIT do > samostatných callbacků, tak bych to řešil dvěma různými tlačítky pro INSERT/EDIT, na které bych navěsil samostatné callbacky. Vložit > jeden a ten samý formulář pro INSERT/EDIT do různých akcí – tím oddělení callbacků nevyřešíme.

Ne funkčnosti, ale práva.
Obecně například komentáře může přidávat kdokoliv přihlášený, tedy práva jsou $this->user->isAllowed('comment', 'insert')
a editovat můžeš jen svoje, pokud nejsi admin, takže práva vypadají takto: $this->user->isAllowed($comment, 'edit')
Pokud by jsi to měl v 1 metodě a je jedno co za kód to je, tak by vypadal asi nějak takto:

#[Inject] public CommentRepository $commentRepo;
private ?Comment $comment = null;
public function actionModify(?int $id = null): void // modify v tomto případě označuje edit/add
{
	// ADD START
	if ($id === null) {
		if (!$this->isAllowed('comment', 'add')) {
			$this->error('Nemáte práva přiávat komenty přihlašte se', 401);
		}
		return;
	}
	// ADD END

	// EDIT START
	$this->comment = $this->commentRepo->get($id);
	if (!$this->comment) {
		$this->error('Komentář nenalezen', 404);
	}
	if (!$this->isAllowed($this->comment, 'edit')) {
		$this->error('Nemáte právo editovat tento komentář.', 401);
	}
	// EDIT END
}

vs správně

#[Inject] public CommentRepository $commentRepo;
private ?Comment $comment = null;

public function actionAdd(): void
{
	if (!$this->isAllowed('comment', 'add')) {
		$this->error('Nemáte práva přiávat komenty přihlašte se', 401);
	}
}

public function actionEdit(int $id): void
{
	$this->comment = $this->commentRepo->get($id);
	if (!$this->comment) {
		$this->error('Komentář nenalezen', 404);
	}
	if (!$this->isAllowed($this->comment, 'edit')) {
		$this->error('Nemáte právo editovat tento komentář.', 401);
	}
}

Tak vidíš, že první případ najednou dělá if mezi 2 různými kódy, i když zbytek aplikace se chová pořád stejně. Máš jeden formulář na přidání i editaci.

Co je ještě důležité je to, že pokud budeš chtít aby uživatel viděl URL ve formátu
/komentar/pridat
/komentar/editovat/5
a napíšeš router takto (což už jsem hodněkrát viděl):

$router->addRoute('komentar/pridat', 'Comment:modify');
$router->addRoute('komentar/editovat/<id>', 'Comment:modify');

Tak přes tuto URL se dostanu do editace, což není úplně ok:
/komentar/pridat?id=5
Stejně tak tato URL vyhodí Error, což by neměla, protože parametr ID by se v přidání měl úplně ignorovat:
/komentar/pridat?id=-3

Takže cpát akce dokupy je špatná praktika.

b ) Chování DELETE jako EDIT

Nějak tomu nerozumím. Mazací tlačítko klidně můžeš dát na editační stránku tak jak to máš teď, ale místo na tvorbu a vyhodnocování celého formuláře to bude prostě jen nastylovaný odkaz, který bude odkazovat na mazací handle/action, jak jsem psal výše. Pro uživatele se funkčnost nezmění, jen ty budeš mít přepsáním lepší kód.

c ) Možná bychom to všechno mohli někdy probrat v hospodě u sklenky oroseného…

Bezva nápad. Akorát nemám moc možností cestovat. :D Práce mi zabírá téměř veškerý volný čas. No a jelikož já jsem od Ostravy a ty z Prahy, tak to moc růžově nevidím.

No a k tvojí reakci na Michala bych napsal asi jen to, že mu nešlo o to napsat sem dokonalý kód, který ukáže top, jak má vypadat aplikace zabezpečeně, ale jen zhruba nástřel, jak se používají komponenty.
Pokud chceš o komponentách něco víc vědět, tak pak buď článek na Planette, nebo kurz pokročilých na learn2code, případně školení od Davida, jestli tam má něco na formuláře/komponenty.

Editoval Polki (18. 10. 2021 13:43)

m.brecher
Generous Backer | 758
+
0
-

@Polki

Také už si začínám myslet, že bude nakonec lepší oddělit akce EDIT a ADD:

  • vidím, že je to zavedená praxe v komunitě vývojářů
  • je to přehlednější a logičtější
  • lépe se řeší práva to jo
  • kódu nebude o moc více
  • ubyde jeden ošklivý if :)
David Grudl
Nette Core | 8133
+
+3
-

Jen poznámka: není dobré metody pro obsluhu formuláře nazývat handle***, protože to má v Nette speciální význam.