nette/application 3.2: atribut #[Requires]

David Grudl
Nette Core | 8218
+
+14
-

Přidal jsem do Nette Application 3.2 nový atribut #[Requires]. (Zatím v 3.2.x-dev větvi).

Pomocí tohoto atributu lze například povolit pro přístup k presenteru jen určité HTTP metody:

#[Requires(methods: 'POST')]
class MyPresenter extends Nette\Application\UI\Presenter
{
}

#[Requires(methods: ['GET', 'POST'])]
class MyPresenter extends Nette\Application\UI\Presenter
{
}

(Toto nastavení obchází a nahrazuje pole $allowedMethods v presenteru.)

Lze vyžadovat AJAXový požadavek`:

#[Requires(ajax: true)]
class MyPresenter extends Nette\Application\UI\Presenter
{
}

Lze vyžadovat jen přístup ze stejného domény:

#[Requires(sameOrigin: true)]
class MyPresenter extends Nette\Application\UI\Presenter
{
}

Lze vyžadovat, že na presenter lze přistoupit jen nepřímo přes forward():

#[Requires(forward: true)]
class MyPresenter extends Nette\Application\UI\Presenter
{
}

Lze povolit přístup jen k některým akcím:

#[Requires(actions: 'default')]
class MyPresenter extends Nette\Application\UI\Presenter
{
}

#[Requires(actions: ['add', 'edit'])]
class MyPresenter extends Nette\Application\UI\Presenter
{
}

Všechny tyto hodnoty lze kombinovat.

Atributy u metod

Využití se stává mnohem zajímavější díky tomu, že lze atribut lepit nejen na třídu, ale také na tyto metody:

  • action<Name>()
  • render<Name>()
  • handle<Name>()
  • createComponent<Name>()

Poslední dvě metody se týkají také všech komponent!

Takže třeba lze kontrolovat, že akce je proveditelná jen AJAXovým POST požadavkem:

	#[Requires(methods: 'POST', ajax: true)]
	public function actionDelete(int $id)
	{
		...
	}

Nebo že render-metoda bude dostupná jen tehdy, pokud se na ni přistoupí nepřímo (forward nebo setView() v action metodě):

	#[Requires(forward: true)]
	public function renderNotFound()
	{
		...
	}

Nebo třeba kontrolovat, že komponenta bude dostupná jen v určitých akcích:

	#[Requires(actions: ['add', 'edit'])]
	public function createComponentPostForm()
	{
		...
	}

U handle metod #[Requires(sameOrigin: false)] nahrazuje atribut #[CrossOrigin].

Alternativní zápis

Atributy lze zapisovat i tímto způsobem:

	#[Requires(methods: 'POST')]
	#[Requires(ajax: true)]
	public function actionDelete(int $id)
	{
		...
	}

Vlastní kombinace

Atribut můžete také podědit a pod jedním názvem mít konkrétní konfiguraci:

#[\Attribute]
class AllowAllMethods extends Nette\Application\Attributes\Requires
{
	public function __construct()
	{
		parent::__construct(methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']);
	}
}

#[AllowAllMethods]
class MyPresenter extends Nette\Application\UI\Presenter
{
}

Budu rád, když to otestujete a napíšete připomínky a podněty.

Aktualizace: finální popis fungování najdete na https://doc.nette.org/…ute-requires

Felix
Nette Core | 1196
+
0
-

Super!

Btw, smerujes tim na vetsi podporu tvorby API v presenterech?

Miellap
Člen | 12
+
0
-

Bylo by skvělé, kdyby se toto chování rozšířilo ještě o omezení komponent v akcích presenteru. Něco jako:

class MyPresenter extends Nette\Application\UI\Presenter
{
	#[Requires(components: ['postForm'])]
	public function actionPost()
	{
		...
	}
}

Tímto by se omezilo to, že akce post může tvořit pouze komponentu postForm. V rámci mého kódu nedává smysl, aby se o toto omezení starala komponenta (jaké akce ji můžou tvořit), protože komponentu využívám v různých Presenterech a různých akcích a tím pádem by toto omezení postrádalo význam. (metody jako createComponentPostForm mám v traitách, abych zbytečně nepsala duplicitní kód)

Mimochodem, lze tento atribut využít i jinde než v Presenterech? Například handle metodách u komponent?

Editoval Miellap (16. 4. 22:21)

ViPEr*CZ*
Člen | 817
+
0
-

#[Requires(actions: (‚add‘, ‚edit‘])]
vs
#[Requires(actions: (‚add‘, ‚edit‘) )]
nebo
#[Requires(actions: [‚add‘, ‚edit‘] )]

Kdyz pouziju
#[Requires(methods: ‚POST‘, ajax: true)]

a poslu to non ajaxem, tak to hodi 5-ti kilo? Nebo 404-ku?

nightfish
Člen | 516
+
0
-

ViPErCZ napsal(a):
#[Requires(actions: ('add', 'edit'])]

Nevybalancované závorky → syntax error.

vs
#[Requires(actions: ('add', 'edit') )]

('add', 'edit') není platný zápis hodnoty v PHP → syntax error. Možná by fungovalo actions: array('add', 'edit'), ale to je fujfuj.

nebo
#[Requires(actions: ['add', 'edit'] )]

Tohle vypadá jako správná syntaxe, jen bych za uzavírací hranatou závorkou nedělal mezeru.

Kdyz pouziju
#[Requires(methods: ‚POST‘, ajax: true)]

a poslu to non ajaxem, tak to hodi 5-ti kilo? Nebo 404-ku?

403 Forbidden

ViPEr*CZ*
Člen | 817
+
0
-

@nightfish samozrejme vim, jak to je spravne… ostatne pokus omylem to taky jde zjistit :-D stejne jako 403-ka ;-)
Ale diky, aspon to maji pripadne juniori rovnou na dlani. ;-)

Btw…

#[Requires(methods: ‚POST‘, ajax: true)]
public function actionDelete(int $id)
{
 …
}

tady me napada, ze by v tom Required mohlo byt neco jak ma Symfony v Route
requirements={„id“=„\d+“}

a to by mohlo hodit 404-ku pokud to neuvedu v url.
Husty by bylo, kdyby to rovnou hazelo ActiveRow a v pripade, ze to ma wrong id, tak 404-ku.
Takhle to hodi TypeError jestli se nepletu, kdyz v URL nebude uvedeno id.

m.brecher
Generous Backer | 863
+
+1
-

@ViPEr*CZ*

Takhle to hodi TypeError jestli se nepletu, kdyz v URL nebude uvedeno id.

Ve starších verzích Nette presenter TypeError vyhazoval v případě, že v metodě akci bylo požadováno int $id a v url id nebylo. To se ale změnilo cca před 2 lety a dnes presentery v této situaci vyhazují 404.

David Grudl
Nette Core | 8218
+
+2
-

Pokud nejsou splněné podmínky, chybí povinné parametry, parametry nejsou požadovaného typu (například int), tak to hodí 40× chybu, nikdy ne 500.

Je možné, že tam někdy byla krátce nějaká chyba a že to hodilo 500, ale to samozřejmě nebyl záměr.

Regulární výrazy se dají psát v routách, tam je to hodně užitečné. U action/render/handle metod se dají používat jen datové typy (pro zajímavost už od první verze Nette, dřív než to mělo PHP :-) ). Nikdy jsem v praxi nenarazil na situaci, že by se mi hodilo argument kontrolovat podle regulárního výrazu, proto to v Nette není.

U číselných hodnot může být užitečné přidat omezení na kladná, nezáporná apod. Ale zase, v praxi nenarážím na situace, že by to bylo skutečně užitečné. Protože ta kontrola proběhne stejně následně, například při pokusu čtení ID z databáze, takže by to fungovalo stejně s případnou anotací jako bez ní.

David Grudl
Nette Core | 8218
+
+3
-

Miellap napsal(a):

Bylo by skvělé, kdyby se toto chování rozšířilo ještě o omezení komponent v akcích presenteru. Něco jako:

	#[Requires(components: ['postForm'])]
	public function actionPost()
	{
		...
	}

Rozumím kam míříš, ale tohle chce lépe promyslet. Jednak by to asi chtělo jinak pojmenovaný atribut, protože nechceme říct, že akce vyžaduje komponentu, ale naopak, že komponenta je povolena pouze v této akci. Tím vlastně by se měla stát automaticky zakázaná v jakékoliv jiné, i takové, která nemá žádnou metodu render/view.

Mimochodem, lze tento atribut využít i jinde než v Presenterech? Například handle metodách u komponent?

Jo, u komponent lze atributovat tyto metody:

  • handle<Name>()
  • createComponent<Name>()
Marek Bartoš
Nette Blogger | 1260
+
0
-

Co třeba #[ForAction(['default'])] nad createComponent*()? Komponent může být v presenteru hodně a lépe se mi bude kontrolovat nad komponentou pro jakou je akci, než naopak.

Pro obecné komponenty, které jsou v base presenteru nebo pro komponenty přidávané dynamicky (přetížením createComponent()) by explicitní seznam všech povolených komponent byl dost nepoužitelný.

Striktně pro všechny createComponent*() může atribut vynutit jednoduché phpstan pravidlo. A aby takové pravidlo nebylo příliš omezující, tak by mohlo fungovat #[ForAction(all: true)] pro povolení všech akcí. To by efektivně fungovalo stejně, jako by atribut vůbec nebyl přidán.

Editoval Marek Bartoš (17. 4. 17:38)

David Grudl
Nette Core | 8218
+
+1
-

@MarekBartoš od toho je právě #[Requires(actions: 'default')] nad createComponent*().

Pokud si přetěžuju createComponent(), tak myslím, že si i snadno ohlídám aktuální akci.

mystik
Člen | 308
+
+2
-

@DavidGrudl Co pridat #ComponentRequires(component: postForm, action:post) na urovni presenteru. Pak by to slo doplnit u kazdeho presenteru dle potreby i kdyz createComponent metkdy budou zdedene nebo z traity

David Grudl
Nette Core | 8218
+
+1
-

@mystik nějak tak to udělám.

Miellap
Člen | 12
+
+1
-

David Grudl napsal(a):

Miellap napsal(a):

Bylo by skvělé, kdyby se toto chování rozšířilo ještě o omezení komponent v akcích presenteru. Něco jako:

	#[Requires(components: ['postForm'])]
	public function actionPost()
	{
		...
	}

Rozumím kam míříš, ale tohle chce lépe promyslet. Jednak by to asi chtělo jinak pojmenovaný atribut, protože nechceme říct, že akce vyžaduje komponentu, ale naopak, že komponenta je povolena pouze v této akci. Tím vlastně by se měla stát automaticky zakázaná v jakékoliv jiné, i takové, která nemá žádnou metodu render/view.

Mimochodem, lze tento atribut využít i jinde než v Presenterech? Například handle metodách u komponent?

Jo, u komponent lze atributovat tyto metody:

  • handle<Name>()
  • createComponent<Name>()

Je pravda, že ten název může být zavádějící (uvažovala jsem nad tím tak, že akce potřebuje zmíněné komponenty aby se načetla správně). Vyřešila jsem to dočasně vlastním atributem s názvem Allow, který lze volat buď nad presenterem nebo zmíněnou akcí. Vypadá to nějak takto:

#[Allow(action: 'post', components: 'postForm')]
#[Allow(components: ['otherForm', 'otherComponent'], handles: 'delete')]
class SettingPresenter extends Presenter
{
    #[Allow(components: 'gridComponent')]
    public function actionGrid(): void
    {
    }
}

Pokud neuvedu u atributu Allow u presenteru akci jsou uvedené komponenty dostupné všem akcím presenteru (v routeru mám vyřešeno, že nelze přistoupit k neexistujícím akcím) Pokud je atribut u akce, je dostupný jen pro ni. Obdobně mi to umožňuje povolit volat Handle metody jen z určitých akci.

Editoval Miellap (19. 4. 9:29)

David Grudl
Nette Core | 8218
+
+1
-

Teď zbývá využít tento atribut pro vyžadování přihlášeného uživatele.

Miellap
Člen | 12
+
0
-

David Grudl napsal(a):

Teď zbývá využít tento atribut pro vyžadování přihlášeného uživatele.

To už taky mám vyřešeno, pokud se ten atribut Allow zadá s parametrem resource, případně privilege, tak to ověřuje vůči autorizátoru a povolím tvoření dané komponenty z dané akce jen na základě tohoto omezení.

David Grudl
Nette Core | 8218
+
+1
-

Jeden z možných příkladů využití. Na webech, zejména v administracích, by správně neměly být akce měnící stav serveru prováděny HTTP metodou GET. Koneckonců proto se jmenuje GET. Takže třeba pro mazání záznamů v tabulce bych měl použít odkazy s metodou POST (ideální by byla metoda DELETE, ale takový požadavek lze vytvořit jen JavaScriptem, takže se historicky používá POST).

Někde na začátku šablony si vytvořím pomocný formulář s id postForm:

@layout.latte

<form method="post" id="postForm"></form>

A ten pak budu využívat pro mazací odkazy. Místo elementu <a> použiju <button>, který lze ale klidně nastylovat jako klasický odkaz, třeba Bootstrap CSS k tomu má třídy btn btn-link.

admin.latte

<table>
	<tr n:foreach="$posts as $post">
		<td>{$post->title}</td>
		<td>
			<button class="btn btn-link" form="deleteForm" formaction="{link delete $post->id}">delete</button>
			<!-- instead of <a n:href="delete $post->id">delete</a> -->
		</td>
	</tr>
</table>

Pro mazání se volá akce delete. A tady právě využiju atribut #[Requires] abych zajistil, že jinak než POST metodou a navíc jen ze stejné domény (obrana před CSRF) požadavek přijít nemůže:

#[Requires(methods: 'POST', sameOrigin: true)]
public function actionDelete(int $id): void
{
	$this->facade->deletePost($id);
	$this->redirect('default');
}

Pokud by mazání realizoval signál, tak není potřeba uvádět sameOrigin: true, protože to mají všechny signály defaultně:

#[Requires(methods: 'POST')]
public function handleDelete(int $id): void
{
	$this->facade->deletePost($id);
	$this->redirect('this');
}

Viz https://doc.nette.org/…s/post-links

Miellap
Člen | 12
+
-1
-

@DavidGrudl
Zkouším teď použít atribut Requires ve své aplikaci a narazila jsem na menší problém, kdy se v error message vypisuje celá cesta a tím se ukazuje adresářová struktura aplikace. Bylo by možné v error message ponechat jen "Method $method is not allowed"?
Případně přidat do atributu nepovinný parametr $errorMessage?

Editoval Miellap (24. 4. 21:28)

David Grudl
Nette Core | 8218
+
0
-

@Miellap Mělo by tam být jen to co píšeš https://github.com/…ssPolicy.php#L120

Miellap
Člen | 12
+
0
-

David Grudl napsal(a):

@Miellap Mělo by tam být jen to co píšeš https://github.com/…ssPolicy.php#L120

$this->presenter->error(
	"Method $method is not allowed by " . Reflection::toString($this->element),
	Nette\Http\IResponse::S405_MethodNotAllowed,
);

Ta cesta se zobrazuje právě v té části Reflection::toString($this->element).. Nechci uživatelům ukazovat ani částečně strukturu aplikace, proto bych ocenila možnost vlastní hlášky.

Marek Znojil
Člen | 84
+
+2
-

Tohle bych spíše viděl na práci error prezenteru, aby mi zobrazil hlášku jak chci dle kódu či typu výjimky.

Miellap
Člen | 12
+
0
-

Marek Znojil napsal(a):

Tohle bych spíše viděl na práci error prezenteru, aby mi zobrazil hlášku jak chci dle kódu či typu výjimky.

Vím kam míříš, ale pokud bych chtěla pro jednotlivé errory různé, konkrétnější hlášky, podle toho co daný error způsobilo (například u error 404 chci různou hlášku podle toho, zda je chybná url nebo nebyl nalezen uživatel), dělalo by se toto rozlišení v error presenter obtížně, proto si raději nastavuji „správné“ chybové hlášky už v rámci aplikace. Hlavně při tvorbě API chci mít error hlášku přesnou, aby bylo hned jasné, co se pokazilo.

Samozřejmě si můžu vyhazovat vlastní výjimku, kterou v error presenteru rozpoznám a odchytím, ale to mi znemožňuje využití Requires atributu.

David Grudl
Nette Core | 8218
+
0
-

@Miellap ověřoval jsem to, a nic z toho co popisuješ se v Nette neděje, pokud se někde zobrazují cesty k souborům, musí to být ve tvém kódu.

Otevři kdyžtak nové vlákno, protože tohle opravdu s atributem Requires nesouvisí.