Přístup k parametrům z requestu v presenteru a komponentách

David Grudl
Nette Core | 8139
+
+8
-

K parametrům z requestu se v presenterech a komponentách přistupuje nejčastěji skrze metody:

function renderDefault(int $id, string $slug = '')
{
	//
}

Výhodou je pohodlí a především typová kontrola.

Pro přístup k parametru odjinud slouží metoda getParameter('id'), ale tam se ztrácí elegance a také typová kontrola. Napadlo mě proto přidat anotaci, která by tohle vyřešila:

use Nette\Application\Attributes\Request;

class MyPresenter extends Nette\Application\UI\Presenter
{
	#[Request]
	public int $id;

	function foo()
	{
		$this->model->getSomething($this->id);
	}
}

V podstatě je to jakási neperzistetní obdoba anotace Persistent.

Máte k tomu nějaké připomínky nebo nápady?

hrach
Člen | 1834
+
+2
-

Na prvni pohled mi nesedi jmeno. Mozna neco jako ByRequest/FromRequest

mystik
Člen | 291
+
+4
-

Nebo #[RequestParameter]

Marek Bartoš
Nette Blogger | 1173
+
+3
-

Já bych bral #[RequestParameter(persistent: false)]

David Grudl
Nette Core | 8139
+
0
-

Marek Bartoš napsal(a):

Já bych bral #[RequestParameter(persistent: false)]

Jelikož to nebude mít konstruktor, nic by nemělo bránit to psát i takto.

Marek Bartoš
Nette Blogger | 1173
+
0
-

Tím jsem chtěl říct, že by bylo fajn mít konzistentní zápis pro persistentní a ne-persistentní parametry. Tedy i #[RequestParameter(persistent: true)] a ne-persistentní variantu mít výchozí.

Editoval Marek Bartoš (29. 9. 2023 17:53)

David Grudl
Nette Core | 8139
+
0
-

Jo, to se bude líp psát i číst, dobrý důvod zavést další atribut pro persistetní parametry.

Pavel Kravčík
Člen | 1182
+
0
-

Takže by mohlo fungovat i tohle, pokud přijdou parametry z ajaxu jako string a přetypují se?

use Nette\Application\Attributes\Request;

class MyPresenter extends Nette\Application\UI\Presenter
{
	#[Request]
	public int $articleId;

	function handleBoo(int $articleId)
	{
		//getEntityByID
	}
}
David Grudl
Nette Core | 8139
+
0
-

Můžeš to rozvést?

m.brecher
Generous Backer | 758
+
0
-

@DavidGrudl

Ano, toto je užitečná feature Nette a určitě ji každý občas použije.

V Nette dokonce již máme přístup k parametrům requestu pomocí atributu a sice atribut #[Persistent]:

#[Persistent]
public string $lang;

Atribut #[Persistent] nedělá nic jiného než navrhovaný atribut #[Request] s tím rozdílem, že parametru navíc přidá persistenci.

Plánovaný atribut #[Request] tak bude v logické dvojici s atributem #[Persistent], budeme mít pro přístup k request parametrům kompletní sadu dvou atributů:

#[Persistent]
string $lang;   // persistentní parametr requestu

#[Request]
public int $id;  // nepersistentní parametr requestu

Přehledné, logické.

Jediné co je zde matoucí je označení nepersistentního parametr requestu jako #[Request], zatímco persistentní parametr, který je také parametrem requestu označení Request nemá.

Protože tyto dva atributy tvoří logickou dvojici, navrhoval bych dát atributu #[Request] takový název, který by se komplementárně hodil k již existujícímu názvu #[Persistent], což by mohlo být třeba #[NonPersistent] nebo #[Local].

Potom bychom měli v záhlaví presenteru parametry pěkně logicky pod sebou:

#[Persistent]
string $lang;

#[Local]
public int $id;

// nebo:

#[NonPersistent]
public int $id;

Asi úplně nejlepší by bylo přidat do obou atributů slovo Parameter, potom by kód byl absolutně nejčitelnější:

#[PersistentParameter]
string $lang;

#[LocalParameter]
public int $id;

Atribut #[PersistentParameter] by mohl být aliasem k již existujícímu atributu #[Persistent], takže by to nemusel hned být BC a až časem by se původní název atributu mohl vyřadit

Editoval m.brecher (29. 9. 2023 22:58)

dsar
Backer | 53
+
0
-

I partially agree with m. brecher. What I liked about Presenters here in Nette is that it's (mostly) abstracted-away from the concept of Request and Response.

Furthermore having #[Persistent] and #[Request] (or #[RequestParameter]) is non-orthogonal.

The contrary of #[Persistent] would be #[Volatile] or #[Mutable]

Pavel Kravčík
Člen | 1182
+
0
-

Pavel Kravčík napsal(a):

Takže by mohlo fungovat i tohle, pokud přijdou parametry z ajaxu jako string a přetypují se?

use Nette\Application\Attributes\Request;

class MyPresenter extends Nette\Application\UI\Presenter
{
	#[Request]
	public int $articleId;

	function handleBoo(int $articleId)
	{
		//getEntityByID
	}
}

V některých projektech jsem si oblíbil v něco podobného:

naja.makeRequest('GET', {link dateAdd!, dateFrom => 'DATE_FROM', dateTo => 'DATE_TO'}.replace('DATE_FROM', dateFrom).replace('DATE_TO', dateTo));

Pokud bych si mohl vynutit, že to bude přetypováno na int třeba atributem, bylo by to super.

m.brecher
Generous Backer | 758
+
0
-

@PavelKravčík

Pokud bych si mohl vynutit, že to bude přetypováno na int třeba atributem, bylo by to super.

Definice typu parametru requestu se provádí typehintem v akci nebo v deklaraci parametru jako property presenteru. Nedává moc smysl deklarovat parametr requestu $id jako property a v akci nepoužít $this->id, ale deklarovat parametr duplicitně přímo v akci – jak máš v ukázce kódu.

Mě dává smysl takovéto použití:

use Nette\Application\Attributes\Request;

class MyPresenter extends Nette\Application\UI\Presenter
{
	#[Request]
	public int $articleId;

	function handleBoo()
	{
		getEntityByID($this->id);
	}
}

Editoval m.brecher (1. 10. 2023 22:32)

m.brecher
Generous Backer | 758
+
0
-

@DavidGrudl

Po diskuzi v Českých Budějovicích souhlasím, že se název atributu #[Local] nehodí a naopak nejlépe zvolený mě připadá #[Request]. Podstatná informace je, že se jedná o parametr requestu. Nekomplementárnost názvu persistentního parametru #[Persistent] by se dala řešit přejmenováním #[Persistent] na #[PersistentRequest].

V této souvislosti upozorňuji, že není v Nette presenterech uspokojivě dořešena otázka persistentního parametru pro držení stavu formuláře. Pokud dnes chci držet v presenteru pro formulář parametr, který by v rámci jedné akce byl persistentní v opakovaných submitech formuláře (např. dynamické přidávání prvků do formuláře), použiji pro to #[Persistent]. Jenže tak jak je #[Persistent] nyní implementován, bude takový parametr persistentní napříč všemi akcemi presenteru, což není žádoucí chování. Ideální by byl atribut na míru formuláře persistentní právě v jedné akci presenteru – v té, ve které je použit. V budoucnu by tak mohly v Nette být 3 různé parametry předané do presenteru pomocí atributů, kde by bylo ideální, aby měly všechny v názvu #[…Request], atribut speciálně pro stav formuláře by se mohl jmenovat třeba #[FormPersistentRequest]

dsar
Backer | 53
+
+2
-

#[Request] is excessively non-orthogonal with #[Persistent].

At this point it would be better to have #[RequestParameter] or even better #[Parameter] with (persistent: true) for the persistent one and deprecate #[Persistent] (or having it as alias)

m.brecher
Generous Backer | 758
+
0
-

@dsar

it would be better to have #[RequestParameter] or even better #[Parameter] with (persistent: true)

the word Parameter is redundant, in case of attributes there is clear from the syntax that it is parameter, second half of your post is interesting fresh idea, so I propose to consider these variants:


// persistent:

#[Request(persistent: true)]
public string $lang

// or

#[RequestPersistent]
public string $lang

// or

#[PersistentRequest]
public string $lang

// non persistent:

#[Request]
public int $id

Editoval m.brecher (2. 10. 2023 3:43)

Pavel Kravčík
Člen | 1182
+
+1
-

@mbrecher

Definice typu parametru requestu se provádí typehintem v akci nebo v deklaraci parametru jako property presenteru. Nedává moc smysl deklarovat parametr requestu $id jako property a v akci nepoužít $this->id, ale deklarovat parametr duplicitně přímo v akci – jak máš v ukázce kódu.

Ano, to je jasný v presenteru. Chtěl bych to v komponentě. Ale je to trochu úkrok stranou od původního vlákna.

/?foo-dateFrom=10&foo-dateTo=20&do=foo-ping bych chtěl obojí dostat jako int (přijde jako string z ajaxu). Pokud bych to dokázal vynutit parametrem v komponentě, bylo by to super.

dsar
Backer | 53
+
0
-

m.brecher napsal(a):
the word Parameter is redundant, in case of attributes there is clear from the syntax that it is parameter

It's not clear from the syntax, since we may have other attributes such as CrossOrigin (and in future AllowedFor)

David Grudl
Nette Core | 8139
+
+11
-

Když tak nad tím přemýšlím, jelikož je to náhrada za getParameter(), adekvátní anotace by byla:

#[Parameter]
public int $id;

Kdo by to považoval za příhodné, mohl by psát #[Persistent, Parameter], ale samozřejmě samostatné Persistent bude fungovat dál.

m.brecher
Generous Backer | 758
+
0
-

@PavelKravčík @DavidGrudl

Ano, to je jasný v presenteru. Chtěl bych to v komponentě.

V ukázce kódu co Jsi poslal není komponenta, ale presenter s akcí, proto jsem tu poznámku nepochopil.

Ano, v komponentě by se úplně stejně hodilo mít atribut #[Parameter] (poslední Davidův návrh na název), který by fungoval analogicky jako již existující atribut #[Persistent] v komponentě, jenom by parametr nebyl persistentní.

Dnes persistentní parametr v komponentě co se týče typování funguje výtečně, předpokládám, že by se analogicky implementoval i #[Parameter]:

// dnes #[Persistent] přetypuje hezky
class MyControl extends Nette\Application\UI\Control
{
	#[Persistent]
    public int $id;

    public function handleSomething()
    {
        $this->id                  // integer
        $this->getParameter('id')  // také integer
    }
}
// zítra #[Parameter] by přetypoval stejně
class MyControl extends Nette\Application\UI\Control
{
	#[Parameter]
    public int $id;

    public function handleSomething()
    {
        $this->id                   // integer
        $this->getParameter('id')  // také integer
    }
}

Předpokládám, že plánovaný atribut bude jak v presenteru, tak v komponentách.

Editoval m.brecher (2. 10. 2023 18:54)

David Grudl
Nette Core | 8139
+
+4
-

Předpokládám, že plánovaný atribut bude jak v presenteru, tak v komponentách.

Název tohoto vlákna a první věta úvodního postu by v tom mohla udělat trošku jasno

David Grudl
Nette Core | 8139
+
+2
-

Už je to k otestování ve vývojové verzi:

composer require nette/application:^3.1.x-dev
Pavel Kravčík
Člen | 1182
+
0
-

@DavidGrudl: Otestoval jsem a funguje to ok (s DI 3.1.6).

Akorát trochu mi zlobí jedna věc, možná to někdo řeší elegantněji. Chápu, že je to asi nesmysl v JS a není to extra důležité a dá se to snadno obejít (int|string), ale kdyby měl někdo nějaký nápad nebo by Nette tenhle případ přetypovalo (intval('PARAM_FROM')===0) bylo by to super.

<?php
declare(strict_types=1);

use Nette\Application\Attributes\Parameter;

class Foo extends \Nette\Application\UI\Control
{
	#[Parameter]
	public int $from = 10;

	#[Parameter]
	public int $to = 20;

	#[Parameter]
	public int $limit = 0;

	public function handlePing(): void
	{
		//output (dump $this->from, $this->to, $this->limit)
	}


	public function handleBing(int $from, int $to): void
	{
		//output (dump $this->from, $this->to, $this->limit)
	}
}

// JS doplňuje hodnoty intFrom = 40, intTo = 50

Na téhle variantě se mi nelíbí '0' jako zástupný znak (s tím zástupným znakem poslední úryvek funguje):

naja.makeRequest('GET', {link ping!, from => '0', to => '1'}.replace('0', intFrom).replace('1', intTo));
//output: 40, 50, 0

Pokud nahradím za něco lepšího, funguje ok:

naja.makeRequest('GET', {link ping!, from => 'PARAM_FROM', to => 'PARAM_TO'}.replace('PARAM_FROM', intFrom).replace('PARAM_TO', intTo));
//output: 40, 50, 0

Akorát se mi nelíbí možnost, upsat se:

naja.makeRequest('GET', {link ping!, from => 'PARAM_FROM', too => 'PARAM_TO'}.replace('PARAM_FROM', intFrom).replace('PARAM_TO', intTo));
//output: 40, 20, 0

Takže by se mi líbilo vynutit si poslání parametrů (např 2 z 3 výše handleBing) a vynutit jeho přetypování:

naja.makeRequest('GET', {link bing!, from => 'PARAM_FROM', too => 'PARAM_TO'}.replace('PARAM_FROM', intFrom).replace('PARAM_TO', intTo));
// output: ERR - must be int, string given
// očekávaný output: ERR - Missing parameter $to required

Finální kód by se mi líbil:

naja.makeRequest('GET', {link bing!, from => 'PARAM_FROM', to => 'PARAM_TO'}.replace('PARAM_FROM', intFrom).replace('PARAM_TO', intTo));
// output: 40, 50, 0

Editoval Pavel Kravčík (19. 10. 2023 14:14)

dakur
Člen | 493
+
+1
-

Jo, tohle jsme taky řešili, že potřebujeme v nette vygenerovat linky s placeholdery a potom je replacovat až v js. My máme všechny parametry jako value objecty, takže potom místo elegantního:

public function actionDefault(CampaignId $campaignId, MaterialId $materialId, PageId $pageId) {}

musíme používat:

public function actionDefault(CampaignId $campaignId, MaterialId $materialId, string $pageId) {
    // a restournout to do VO ručně
	$pageId = PageId::of($pageId);
}

aby byl router schopen sestavit placeholder link:

$this->link('Something:actionDefault', [
    SomethingPresenter::PARAM_CAMPAIGN_ID => $campaignId,
    SomethingPresenter::PARAM_MATERIAL_ID => $materialId,
    SomethingPresenter::PARAM_PAGE_ID => '%placeholder%',
]);

Kdyby na to byl v nette nějaký mechanismus, bylo by to super, ale jak píšeš, je to asi spíš nice-to-have.

m.brecher
Generous Backer | 758
+
0
-

Návrh – implementovat parametr s atributem #[Parameter] jako stavový

Nový atribut #[Parameter] zpřehlední kód a zajistí správné typování. Navrhuji doplnit důležitou feature z hlediska existujícího komponentového modelu Nette. Parametry deklarované pomocí #[Parameter] by měly být automaticky zahrnuty do stavu akce presenteru (stavu komponenty).

#[Parameter]
public ?string $param = null;

by měl být zahrnut do aktuálního stavu akce stejně jako:

public function actionUpdate(?string $param = null)
{}

Argumenty:

Presenter

V presenteru je parametr s atributem #[Parameter] obdobou parameru akce. Není sice v akci deklarovaný, je ale deklarovaný v presenteru a v aktuální akci jej lze použít (libovolné akci). Je proto ze své podstaty součástí aktuálního stavu presenteru , kam patří presenter, akce a parametry akce (aktuální view).

V aktuální verzi Nette parametr #[Parameter] součástí aktuálního stavu presenteru není a signál presenteru, nebo submit formuláře parametr vymaže. To je v rozporu s principem komponentového modelu Nette, že signály komponent NEMĚNÍ aktuální stav akce presenteru.

Proto by bylo vhodné, aby parametr s atributem #[Parameter] byl součástí aktuálního stavu presenteru, a NEMĚNIL se signálem komponenty/submitu formuláře.

Je to navíc i chybějící užitečná funkcionalita pro formuláře. Pokud potřebujeme v parent komponentě formuláře řídící parametr pro formulář (přidání inputu, …), potom je jediné řešení deklarovat jej jako #[Persistent], což má vedlejší efekt – persistenci, která je na obtíž. Tento návrh to elegantně řeší.

Komponenta

V komponentě je situace s #[Parameter] parametrem obdobná jako v presenteru. Rozdíl je v tom, že komponenty nemají akce, ale mají stejně jako presentery stav, který by podřízené komponenty, včetně formulářů neměly měnit. Součástí stavu komponenty jsou stavové parametry komponenty – nyní persistentní parametry komponenty.

Pokud potřebujeme, aby subkomponenty neměnily aktuální stav komponenty, použijeme v komponentě #[Persistent] parametry. Stejně jako v presenteru ale tento atribut dělá víc než obvykle potřebujeme. Zajistí, že subkomponenty nemohou parametr měnit, ale přidají nadbytečnou funkci – persistence v presenteru, parametr se předá i do signálů nadřazené komponenty – to může být nežádoucí.

Tedy i v komponentách by bylo vhodné, aby #[Parameter] parametry byly součástí aktuálního stavu komponenty. Subkomponenty by je nemohly měnit, ale do odkazů nadřazených komponent by se nepřenášely.

Ajax

V ajaxovém payloadu se z Nette aplikace odesílá vedle položky snippets i položka state, kde jsou v současné době přenášeny persistentní parametry. Stavové parametry se mohou hodit v javascriptu zpracovávajícím ajaxový response. Proto si myslím, že by se #[Parameter] parametry měly předávat i v payloadu jako součást stavu presenteru/komponenty.

Ages
Člen | 128
+
0
-

Možná jsem to špatně pochopil, ale pokud by toto mohl být alternativní způsob zápisu parametrů, jak vyřeším případný redirect v Presenteru?

final class OrderPresenter extends Presenter
{
	#[Parameter]
	public int $orderId = 0;

	...

	public function actionFoo(): void
    {
        bdump($this->orderId);
    }

    public function actionBar(): void
    {
		$entity = $this->model->getById($this->orderId);
		$this->redirect('foo', $entity->id); // Toto vyhodí vyjímku: Nette\Application\UI\InvalidLinkException
    }

}
m.brecher
Generous Backer | 758
+
0
-

@Ages

jak vyřeším případný redirect v Presenteru?

Nezkoušel jsem to, ale řekl bych, že se to vyřeší tím, že se paralelně k deklaraci parametru pomocí #[Parameter] uvede parametr jako parametr příslušné akce:

public function actionFoo(int $orderId = 0): void
    {
        bdump($this->orderId);
    }

Je to v podstatě ten samý problém, který popisuji zde: https://forum.nette.org/…komponentach#…

Atribut #[Parameter] je výborná myšlenka, ale není v této chvíli příliš funkční – problém se submitem formuláře, problém s redirectem. Aby se dal bezproblémově používat, je potřeba, aby byl frameworkem považován za parametr aktuální akce.

Ages
Člen | 128
+
0
-

@mbrecher
Jasně toto funguje.
Skutečně jsem to asi špatně pochopil,nemá jít o náhradu parametrů v metodě ale pouze o alternativní přístup k parametru mimo metodu.

Jen mě překvapilo, že v Latte mohu ten parametr přidat i když není definovaný na vstupu akce, ale redirect toto nepovoluje.

final class OrderPresenter extends Presenter
{
	#[Parameter]
	public int $orderId = 0;

	...

	public function actionFoo(): void
    {
        bdump($this->orderId);
    }

    public function actionBar(): void
    {
		$entity = $this->model->getById($this->orderId);
		$this->redirect('foo', $entity->id); // Toto vyhodí vyjímku: Nette\Application\UI\InvalidLinkException
    }

}


/--latte
<a n:href="foo, $id">Akce</a> <! -- Toto nevyvolá chybu -->
\--

Marek Bartoš
Nette Blogger | 1173
+
+2
-

@Ages Ty se spoléháš na pořadí, namísto na názvy parametrů, což může fungovat pro parametry funkce, ale už ne pro properties. Plus by měly být parametry odkazu pole.

$this->redirect('foo', ['orderId' => $entity->id]);

Ten vygenerovaný odkaz v Latte ti skutečně funguje? Výchozí chování při neplatném odkazu je myslím warning, ne exception.

Editoval Marek Bartoš (6. 2. 10:24)

Ages
Člen | 128
+
0
-

Tak, teď mi to je už jasné :-D
Toto veče vyzkouším?
$this->redirect('foo', ['orderId' => $entity->id]);

V latte jsem to měl uvedené s názvem parametru, jen jsem to sem dopsal ve zkrácené verzi.
Pak by tedy bylo vše ok a chovalo by se to v obou případech stejně.

@MarekBartoš děkuji za vysvětlení.

m.brecher
Generous Backer | 758
+
0
-

@Ages

Skutečně jsem to asi špatně pochopil, nemá jít o náhradu parametrů v metodě ale pouze o alternativní přístup k parametru mimo metodu.

Je to tak. Praktická využitelnost takovéto funkce je ovšem otázkou. Má neočekávané chování, že když se deklaruje parametr pomocí #[Parameter] a odešle v nějaké akci formulář, tak parametr zmizí – podobně jako u toho přesměrování. Očekávané chování by bylo, že je to syntakticky jiný způsob deklarace parametru akce.

Copak v presenteru, tam jednoduše použiju starý zápis a je to vyřešeno, ale v komponentách akce nemáme a tak když chci zajistit, aby nějaký parametr přežil submit formuláře, tak mám jedinou možnost použít #[Persistent]. Ten ovšem vše komplikuje, protože přidává nežádoucí persistenci, což vadí, musí se to potom pečlivě ošetřovat v odkazech, jinak komponenty dělají psí kusy. Takže atribut #[Parameter], který by byl plnokrevným stavovým atributem v Nette vyloženě chybí a v komponentách ho není de facto jak nahradit. Protože použít #[Persistent] je jenom dočasné řešení.

Editoval m.brecher (6. 2. 13:05)

Marek Bartoš
Nette Blogger | 1173
+
0
-

Jestli se #[Parameter] nechová stejně jako parametr action/render metody, tak bych to považoval za bug a bylo by fajn založit na to issue, ideálně s jednoduchou demonstrací problému

m.brecher
Generous Backer | 758
+
0
-

@MarekBartoš

Ano, založím issue s podrobným popisem problému.