Používat traity, nebo dědičnost?

Bulldog
Člen | 110
+
0
-

V reakci na tohle vlákno jsem založil toto vlákno, abychom neodbočovali od tématu vlákna.

Taktéž bych se rád k tomu tématu ihned vyjádřil.

Nejdřív bych na pravou míru uvedl pojmy, abychom navzájem věděli o čem se bavíme.

  1. Dědičnost (Inheritance)

    Dědičnost znamená, že potomek umí vše, co rodič + něco navíc. Vzhledem k tomu, že dnešní vývojáři nejsou analytici a firmy nechtějí analytiky platit a vývojáři tedy neumí poznat, kdy potomek opravdu vždy bude potřebovat vše to, co umí rodič, tak se od tohoto konceptu ustupuje, protože pak vznikají třídy, které umí vše a od nich dědí specializované třídy, které využívají zlomek rodičovského potenciálu. Taktéž může nastat specialita zvaná Fragile base class, která zničí půlku kódu jedním mrknutím a ataktéž nastává nesprávným použitím dědičnosti. Proto některé nové jazyky/frameworky dědičnost ani nepodporují (React).

Používá se však typicky na dodržení DRY principu a to, že společné vlastnosti a funkcionalitu potomků vyextrahujeme do rodiče.
Příklad správně navržené dědičnosti je Shape2D jako rodič a Square, Circle, Triangle atp. jako potomci.

  1. Kompozice (Composition)

    Případ, kdy máme 2 nebo více rozdílných tříd, kdy každá něco umí, ale dělají rozdílné věci a tedy nelze použít dědičnost. Taktéž se dá použít na případy, kdy máme dvě třídy, které očividně jsou nadřazená a podřízená, ale bojíme se použít dědičnost z důvodů budoucích úprav. V takovém případě funguje ‚potomek‘ jako proxy třída nad rodičem, případně jako částečná proxy, kdy je jen část potomka proxy a zbytek je přidaná hodnota. Výhoda tohoto přístupu je, že se tím dá nasimulovat vícenásobná dědičnost, která je povolena například v C++, tedy že potomek funguje jako proxy pro vícero tříd (rodičů) naráz.

  2. Traity (Traits)

    Z hlediska OOP neexistují, jak psal @mbrecher zde. Respektive vůbec neexistují. Jde pouze o syntax sugar, který má za cíl v OOP jazycích, které neumí vícenásobnou dědičnost řešit duplicitu kódu, ale řeší to pouze CopyPastou, takže se to spíše podobá C/C++ makrům.

Hlavní důvod vzniku byl, kdy existuje nějaká abstraktní třída, například Vehicle, ze kterého logicky dědí všechny dopravní prostředky.
Ale mějme následující dopravní prosředky:

class Drozd extends Vehicle
{
  public function drive() {...}
  public function swim() {...}
}

class AirCar extends Vehicle
{
  public function drive() {...}
  public function fly() {}
}

class FlyShip extends Vehicle
{
  public function fly() {...}
  public function swim() {...}
}

Všechny jsou dopravní prostředky o tom není pochyb. Ale vždy 2 ze 3 umí něco společného. Takže co? V jazycích podporujících vícenásobnou dědičnost bychom vytvořili ještě třídy jako:

class LandVehicle extends Vehicle
{
  public function drive() {...}
}

class AirVehicle extends Vehicle
{
  public function fly() {...}
}

class WaterVehicle extends Vehicle
{
  public function swim() {...}
}

Ze kterých by konkrétní třídy dědily.

class Drozd extends LandVehicle, WaterVehicle { }

class AirCar extends LandVehicle, AirVehicle { }

class FlyShip extends AirVehicle, WaterVehicle { }

A samozřejmě by vystupovaly v rolích rodičů:

function foo(Vehicle v) {}
function barW(WaterVehicle wv) {}
function barL(LandVehicle lv) {}
function barA(AirVehicle av) {}

Drozd myDrozd = new Drozd();
foo(myDrozd);	// OK
barW(myDrozd);	// OK
barL(myDrozd);	// OK
barA(myDrozd);	// ERROR

Ale v jazycích, které nemají vícenásobnou dědičnost, jimiž je i PHP, nebyla možnost, jak toto vyřešit jinak, než kompozicí, což by zabránilo flexibilitě, omezilo polymorfismus, snížilo výkon (ano kompozice je pomalejší, než dědičnost) a v neposlední řadě to drasticky zvýší duplicitu kódu, protože nemáme jak společné chování vyextrahovat do jednoho místa. A zde nastupují Traity:

trait LandVehicle
{
  public function drive() {...}
}

trait AirVehicle
{
  public function fly() {...}
}

trait WaterVehicle
{
  public function swim() {...}
}

class Drozd extends Vehicle
{
  use LandVehicle;
  use WaterVehicle;
}

class AirCar extends Vehicle
{
  use LandVehicle;
  use AirVehicle;
}

class FlyShip extends Vehicle
{
  use AirVehicle;
  use WaterVehicle;
}

Což nám krásně vyřešilo duplicitu, která se přesunula pouze do fráze use <TraitName>. Vyřešilo to i performance, jelikož traita je nakopírovaná dovnitř třídy a stává se její součástí. Vyřešilo to i flexibilitu, jelikož můžeme třídám dynamicky přidávat vlastnosti nehledě na předka.
Jediné, co to nevyřešilo je polymorfismus, protože neexistuje nic jako AirVehicle a nejde tedy udělat nic jako $drozd instanceof AirVehicle, ani nemůže být traita jako typehint atp. Což nám dost komplikuje situaci, jelikož nemůžeme najednou vyžadovat pouze létající vozidla. Od toho nám ale pomůže mírná úprava kódu, s využitím interfaces:

interface LandVehicle
{
  public function drive();
}

interface AirVehicle
{
  public function fly();
}

interface WaterVehicle
{
  public function swim();
}


trait LandVehicleTrait
{
  public function drive() {...}
}

trait AirVehicleTrait
{
  public function fly() {...}
}

trait WaterVehicleTrait
{
  public function swim() {...}
}


class Drozd extends Vehicle implements LandVehicle, WaterVehicle
{
  use LandVehicleTrait;
  use WaterVehicleTrait;
}

class AirCar extends Vehicle implements LandVehicle, AirVehicle
{
  use LandVehicleTrait;
  use AirVehicleTrait;
}

class FlyShip extends Vehicle implements AirVehicle, WaterVehicle
{
  use AirVehicleTrait;
  use WaterVehicleTrait;
}

Ovšem z pohledu OOP jsou to pořád jen 3 různé třídy, které implementují podobná rozhraní přímo v sobě, ovšem již je zachován DRY princip a taktéž všechny přístupy z pohledu vícenásobné dědičnosti.
Připomeňme si ale, že stejného výsledku v rámci chodu programu bychom dosáhli, pokud bychom místo trait použili kompozici a jednotlivé dopravní prostředky by byly jen proxy třídy. Kód by sice byl nečitelný a neudržitelný, ale z pohledu vnějšího uživatele programu by fungoval stejně.

Bulldog
Člen | 110
+
0
-

Tak když jsme si vysvětlili názvosloví a víme, na co jsou traity (abychom dodrželi DRY princip a dosáhli imitace vícenásobné dědičnosti), tak se můžeme podívat na jejich využití a uvidíme, že Presentery jsou jasný favorit na použití Trait.

Máme zde jeden rodičovský Nette\Application\UI\Presenter, od kterého musí všechny ostatní Presentery dědit.
Taktéž zde máme problém, že když existují komponenty, ze kterých skládáme stránku, tak je vysoce pravděpodobné, že vznikne situace jako výše s vozidly, kdy se komponenty v Presenterech náhodně překrývají a tedy by vznikla potřeba vyextrahovat společný kód do předka, kterých by ale muselo být více a různě překrytých, tedy by vznikla potřeba vícenásobné dědičnosti.

Zde tedy přichází na řadu Traity a tedy každá takováto komponenta by měla svůj kód z Preseteru definovat v Traitě a Presentery by se nakonec měly skládat jen z těchto Trait.
Ovšem existují situace, kdy bychom podle návrhu aplikace některou z komponent potřebovali využít ve VŠECH Presenterech v daném scope.
Například, jak jsem psal v odkazovaném vláknu tohoto fóra, pokud si vytvořím AdminModule s cílem, že do něj mají přístup POUZE administrátoři, tak absolutně nedává smysl mít toto ověření v každém Presenteru patřícím do AdminModulu zvlášť. Na toto přesně je určená dědičnost, když víme, že nenastane případ, kdy by v AdminModulu přibyl presenter, který zde pustí i někoho jiného, než administrátora. Od toho jsou jiné moduly. V tomto případě tedy nad Traitou vyhrála dědičnost.

Obecně toto tvrzení platí, že pokud je aplikace navržená podobným způsobem, například moduly jsou rozděleny podle šablony @layout.latte, tedy přihlašovací obrazovka má vlastní modul Sign, administrátorská část má vlastní modul Admin atp., tak všechny komponenty, ze kterých se skládá daná layout šablona daného Modulu mohou býti obsaženy v abstraktním presenteru pro daný modul, protže díky návrhu víme, že každá změna layoutu bude mít vlastní modul a tedy NEMŮŽE nastat situace, kdy by do daného modulu přibyl presenter, který by nevyužil plný potenciál abstraktního presenteru a tedy zde je dědičnost na místě.

Ovšem může nastat situace, kdy layout z AdminModule a layout například z MainModule využívají stejnou komponentu. V takovém případě opět může nastat výše zmíněný problém s vícenásobnou dědičností a nelze použít nějakého společného předka jako abstraktní presenter napříč všemi moduly.
V tento moment opět přichází na řadu Traita, takže BaseAdminPresenter bude používat stejnou traitu jako BaseMainPresenter, což znamená, že i komponenty se mohou používat napříč moduly, ale měnit se bude jen konkrétní šablona, která se má vykreslit v rámci konkrétního modulu.

Obecně overuse trait není dobrý a to už proto, že třeba může nastat ‚diamantový problém‘ stejně jako u vícenásobné dědičnosti, PHP samo o sobě mnoho trait v jedné třídě špatně snáší a ze zkušenosti vím, že při použití 5 a více trait mi občas PHP náhodně spadne a hlavní důvod proč omezit traity na minimum je, že traity tvoří obří špageti třídy, které mají mnoho metod, které mohou kolidovat a vytvářet již zmiňovaný diamantový problém a taktéž mohou mít za následek stejný problém jako dědičnost a to, že traita bude definovat chování, které ve třídě nepotřebujeme, což má za následek spousty malých trait obsahujících jednu funkci, které se pak šíleně kombinují a snižuje to přehlednost.

m.brecher
Generous Backer | 758
+
0
-

@Bulldog omlouvám se za odbočení od původního tématu vlákna. Díky za profesionální výklad trait do hloubky a de facto ukončení diskuze. Shrnuto jednou větou ideální je rozumná kombinace dědění a kompozice, tedy použití hierarchie presenterů a trait tam, kde se ta která technika nejlépe hodí.

Kamil Valenta
Člen | 762
+
0
-

Já teda do „de facto ukončené diskuze“ vstoupím, když už začala.

Zda jsou traity součástí OOP nebo ne nechávám stranou, je to více o slovíčkaření než o praktickém přínosu. PHP manuál je pod OOP má v sekci Classes and Objects.

Informace o korelaci počtu trait a pádu php je podána zavádějícím způsobem, bez nějakého doložení či eliminace jiných souvislostí. Samozřejmě běžně fungují classy s více než 5 traitami.

Přisuzovat jednomu modulu výhradně jeden layout je značně omezené. Z mého pohledu je i AdminModul antipattern jak hrom, ale to už bych odkláněl diskuzi jinam. Důležité je, že jeden modul prostě více layoutů mít může a často i mívá.
Traitami stále 1 řádek, dědičností další úroveň abstraktních presenterů.

Určitě bych vyzdvihl přehlednost a čitelnost presenteru při použití trait. Tak jako nechceme mít závislosti schované někde v globálním prostoru nebo v 10. prapředku dané třídy. Fakt je někdy sranda 10× kliknout na dědičnost, než člověk najde definici toho, co hledal. Každý presenter se k závislostem přizná a DI mu je dá.
U trait se na konkrétním presenteru podíváš na jeho „use“ a hned je vidět, co umí.

Uvádět jako negativum trait rozpad kódu na menší celky už je hodně subjektivní.

Marek Bartoš
Nette Blogger | 1171
+
+1
-

@m.brecher K tvému případu, kdy traity použít „nejde“

trait CanonizerImpl
{

	final public function injectCanonizer(Canonizer $canonizer): void
	{
        $this->onStartup[] = fn() => $canonizer->canonize();
	}

}

Editoval Marek Bartoš (17. 10. 2022 0:13)

Bulldog
Člen | 110
+
+2
-

@MarekBartoš
Znásilňovat inject metody, abych spustil určité chování sice jde a viděl jsem jak to doporučoval i David na nějakém příkladu, ale mě osobně to jde silně proti srsti. Nerozumím proč bych měl něco nějak používat jen proto, že to jde a ne pro to, na co to bylo vytvořeno.

Samozřejmě jdou vyřešit případy, kdy existují kolize mezi názvy funkcí trait (ať už mezi sebou navzájem, nebo traita vs třída) a na to mají traity přímo funkčnost:

trait CanonizerImpl
{
    #[Inject]
    public Canonizer $canonizer;

    startup()
    {
        parent::startup();
    	$this->canonizer->canonize();
    }
}
final class ArticlePresenter extends Nette\Application\UI\Presenter
{
	use CanonizerImpl {
		startup as canonizerImplStartup
	};

	startup()
    {
		$this->canonizerImplStartup();	// Parent::startup už volá traita...
    }
}

David to sice doporučoval a používal metodu inject bez suffixu na nastavování Presenteru, ale myslím si, že bychom měli dodržovat principy názvosloví method a tedy metoda by měla dělat přesně to, co odpovídá jejímu názvu a tedy metoda, která se nazývá inject by měla něco injectovat a toť vše.

Pokud pro tyto případy do Nette nepřibudou jiné metody, které budou mít třeba prefix setup, tak mi jde proti srsti na to používat inject a radši se smířím s delším overridingem názvu.

@mbrecher
To nemělo být ukončení diskuse, ale začátek :D Aby každý konstruktivně předložil to, jak to dělá a dal výhody, nevýhody a aby si z toho každý vzal to nejlepší.

@KamilValenta
V zásadě s tebou souhlasím.
Nicméně jak psal m.brecher, tak záleží hodně na tom, jak k projektu přistupuješ. Projekt si nežije vlastním životem a ty jako programátor namáš z mého pohledu povinnost ohýbat kód tak, aby vyhovoval projektu. – jestli jsem tě špatně pochopil, tak mi dej prosím vědět -
Ale projekt a jeho návrh by měl korespondovat nějakým pravidlům, které se na začátku nastaví. Díky tomu nám nikdy nevznikla nutnost mít v rámci jednoho modulu vícero layoutů. A i kdyby nastala, tak od toho máme v rámci Nette možnost mít moduly v modulech. Nemusíme nutně tvořit modul zvlášť a roztrhnout AdminModule na dva, ale v AdminModulu vytvoříš dva podmoduly.

Viděl jsem různé způsoby dělení aplikací do modulů, ale všechny mi přišly krkolomné a neseděly mi. Jelikož díky nastaveným pravidlům jsem nikdy nepotřeboval to mít jinak, tak stále preferuji dělení modulů podle vzhledů layoutu.
Tedy to, že napíšeš, že ‚Důležité je, že jeden modul prostě více layoutů mít může a často i mívá.‘, tak to právě záleží na návrhu aplikace a ne na tom, že to může nastat. Respektive pokud navrhneš aplikaci tak, že z jejího principu to nejde, tak to neplatí. Pokud se ale budeš řídit tím, že to jde a ohýbat na to kód, tak pak ano. Řekl bych, že to je o přístupu. Ale je to jen můj názor.

Já to vidím tak, že i traktor může být použit jako auto pro závody Dakar, ale není na to tvořeno, tak nevidím důvod, proč mu dávat coby kdyby speciální sadu pneumatik, náhradní závodní motor atp. Prostě jsem si předem určil, že traktor je pro práci na poli, tak to tak bude nehledě co. A když někdo bude chtít auto na závody Dakar, tak mu postavím auto na míru. Ale nebudu na to ohýbat ten traktor jen proto, že ho někdo na ty závody použít může, což může. Já jsem ho určil, že na to dělaný není a tím to hasne.

Takže pro mě je určeno, že modul reflektuje jeho layout a nový layout jde do nového modulu. Nebudu kvůli tomu ohýbat existující modul starající se o jiný layout. :)

Určitě bych vyzdvihl přehlednost a čitelnost presenteru při použití trait. Tak jako nechceme mít závislosti schované někde v globálním prostoru nebo v 10. prapředku dané třídy. Fakt je někdy sranda 10× kliknout na dědičnost, než člověk najde definici toho, co hledal. Každý presenter se k závislostem přizná a DI mu je dá.

Tohle, co jsi napsal je přesně příklad špatně použité dědičnosti. Správná dědičnost je taková, kde potomci nepřepisují metody rodiče, ale doplňují rodiče o vlastní. Jak jsem psal, tak dědičnost je funkčnost rodiče + funkčnost potomka. Nikde se nepíše o tom, že by potomek měl upravovat rodiče. Pokud jej upravit musí, tak je dědičnost špatně použitá a měla by se použít třeba kompozice a návrhový vzor Decorator + interface implementovaný v obou třídách. A to hlavně proto, že při method overridingu se pak dá dost špatně dodržet Liskov substitution principle a tedy si nemůžeš být jistý, co vlastně ta metoda, kterou voláš udělá. V případě použití interface však počítáš s tím, že když tu metodu zavoláš, tak se může její vykonání lišit (Další důvod, proč v názvech rozlišovat, co je interface :D )

Když tedy správně použiješ dědičnost, tak ten problém s 10 prokliky nikdy mít nebudeš, jelikož každý proklik bude směřovat přímo na BaseMethod. Hezky je to popsáno třeba tady, tady, nebo tady atd.

Samozřejmě běžně fungují classy s více než 5 traitami.

Fungují. Ale jsou/byly tam problémy. Občas se mi prostě stávalo, že se některé z těch trait neaplikovaly, ale po refreshi ano a po dalším zase ne. (dostával jsem Call to undefined method error) a odzkoušením na milionech requestů jsem tehdy zjistil, že se to stává pouze pokud mám v use více, než 5 trait

EDIT
Samozřejmě pokud je funkce abstraktní, nebo přímo dělaná k tomu, aby potomek do ní přidal funkčnost, jako jsou v Presenterech startup metody, případně beforeRender metody, tak zde je overriding na místě a máš pravdu, že tam je peklo mít takových úrovní víc. Opět ale záleží na návrhu. Pokud máš v rámci každého Modulu BasePresenter, tak je jasné, že ověřovat, kdo může do AdminModulu budeš asi v BasePresenteru v AdminModulu a ne v nadřazeném presenteru/modulu atp.
Tedy v rámci cíleného přetěžování metod a správné hierarchie a návrhu dokážeš vždy trackovat/vědět, kde se jaký kód vykonává a tedy problém 10 zanoření zde opět není.

Editoval Bulldog (18. 10. 2022 2:37)

m.brecher
Generous Backer | 758
+
0
-

@Bulldog

Ahoj, ohledně „zasouvání“ komponent do presenterů pomocí use trait bych měl dotaz na provozní zkušenosti s komponentami v traitách a nějaké detaily provedení.

Na fóru a v dokumentaci Nette se prosazují 3 základní návrhové vzory jak vkládat komponenty a formuláře do presenterů.

a) createComponent<Component>() + new Form/Component v presenteru

b) createComponent<Component>() + $this->componentFactory->create() v presenteru + třída Factory bokem

c) u formulářů ještě třída dědící z Control která uvnitř obsahuje formulář + totéž co v b)

U všech tří způsobů je v presenteru nějaký kód, u způsobu a) se před presenter do komponenty předávají závislosti, b) a c) umožňuje předat závislosti pomocí Factory přímo. c) u formulářů umožňuje věci, které formuláře neumí – např. persistentní parametry, nebo oddělené vykreslování.

Jak píšeš, že traita nastoupila jako dobrá možnost pro zjednodušení kódu presenterů a znovupoužití komponent, tak mě napadá, že traitu u komponent můžeme jednak použít pro odsunutí části kódu stranou – a to jak kód komponenty – factory i obslužné handlery eventů formulářů nebo signálů, tak také injektování závislostí pro komponentu. Napadá mě, že použijeme-li pro vložení komponenty do presenteru traitu, tak Factory komponenty ztrácí smysl. Je to tak?

Jinak moc díky za obsáhlé a fundované vysvětlení problematiky.

m.brecher
Generous Backer | 758
+
0
-

@MarekBartoš Ano, chvíli poté co jsem postnul svoje tvrzení o tom co traity neumí jsem si říkal, že to možná nějak půjde, já traity moc neznám. Ale nejde to snadno a přímočaře, ten kód funguje, ale je hůře čitelný.

Nicméně jak začínám do trait pronikat, tak už jim nějakou smysluplnou šanci na komponenty dávám. Ale v této chvíli odhaduji, že co se komponent týče asi nepřinesou oproti zavedené praxi komponenta + factory nic navíc, možná ten kód bude o něco jednodušší, ale to budu moct říct až si to důkladně vyzkouším.

m.brecher
Generous Backer | 758
+
0
-

@KamilValenta K layoutům bych podotkl, že hodně záleží na tom, jak vlastně layout stavíš. Pokud layoutem řešíš třeba nějaké části menu v administraci, tak potom souhlasím, že v jednom modulu může být více layoutů. Moduly jsou o seskupení souvisejících presenterů a komponent ideálně se stejnými uživatelskými právy. Layout nemusí nutně moduly kopírovat. Kvůli více layoutům myslím nestojí za to vytvářet další vrstvu v hierarchii presenterů. Tam traity asi splní svůj účel perfektně – nebo komponenty. Layout lze pojmout i jako komponentu.

V Nette moc projektů za sebou nemám, ale myslím, že koncept, že má administrace jeden hlavní layout a několik podlayoutů je u složitějších aplikací dobrý koncept. Až to budu někdy dělat, vzpomenu si na traity ;).

Editoval m.brecher (17. 10. 2022 4:30)

Bulldog
Člen | 110
+
0
-

Tvorbu formuláře třeba dělám takto:
FormFactory (registrovaná do DIC)

<?php declare(strict_types=1);

namespace App\Components\Sign\In;

use App\Components\FormFactory as BaseFormFactory;
use App\Model\Db\Repository\UserRepository;
use Nette\Application\UI\Form;
use Nette\Security\AuthenticationException;
use Nette\Security\User;
use Nette\SmartObject;

class FormFactory
{
    use SmartObject;

    public function __construct(
        private readonly BaseFormFactory $formFactory,
        private readonly User $user,
        private readonly UserRepository $userRepository,
    )
    {
    }

    public function create(): Form
    {
		// Vytvoří předdefinovaný formulář, například s vlastním rendererem, translatorem atp.
        $form = $this->formFactory->create();

        $form->addEmail('email', 'E-mail')
            ->setRequired();
        $form->addPassword('password', 'Password')
            ->setRequired();

        $form->onValidate[] = $this->onValidate(...);
        $form->onSuccess[] = $this->onSuccess(...);

        $form->addSubmit('submit', 'Login');

        return $form;
    }

    private function onValidate(Form $form): void
    {
        $values = $form->getUntrustedValues('array');

        if (!$this->userRepository->getByEmail($values['email'])) {
            $form->getComponent('email')
                ->addError('Wrong credentials');
        }
    }

    private function onSuccess(Form $form): void
    {
        $values = $form->getValues();

        try {
            $this->user->login(
                $values->email,
                $values->password,
            );
        } catch (AuthenticationException $e) {
            $form->getComponent('email')
                ->addError('Wrong credentials');
        }
    }
}

SignInFormControl:

<?php declare(strict_types=1);

namespace App\Components\Sign\In;

use Closure;
use Nette\Application\UI\Control as UiControl;
use Nette\Application\UI\Form;

class Control extends UiControl
{
    public function __construct(
        private readonly FormFactory $formFactory,
        private readonly Closure $onSuccess,
    )
    {
    }

    public function render(): void
    {
        $this->template->render(__DIR__ . '/default.latte');
    }

    public function createComponentForm(): Form
    {
        $form = $this->formFactory->create();

        $form->onSubmit[] = $this->onSubmit(...);
        $form->onSuccess[] = $this->onSuccess;  // Aby si mohl každý kdo používá formulář nastavit vlastní successCallback

        return $form;
    }

    private function onSubmit(): void
    {
        $this->redrawControl('form');
    }
}

šablona default.latte v komponentě:

{snippet form}
	{control form}
{/snippet}

továrna na komponentu (registrovaná do DIC):

<?php declare(strict_types=1);

namespace App\Components\Sign\In;

use Closure;

interface ControlFactory
{
    public function create(
        Closure $onSuccess,
    ): Control;
}

traita:

<?php declare(strict_types=1);

namespace App\Components\Sign\In;

use Nette\DI\Attributes\Inject;

trait PresenterTrait
{
	// metody a property musí být unikátně pojmenované, aby nevznikaly kolize, takže na pojmenování používám namespace+classname
    #[Inject]
	public ControlFactory $signInFormControlFactory;

    public function createComponentSignInFormControl(): Control
    {
        return $this->signInFormControlFactory
			->create($this->onSignInFormSuccess(...));
    }

	// Tato metoda může být přidána do komponenty vícero způsoby.
	// To, že já používám konstruktorovou závislost nic neznamená
    private function onSignInFormSuccess(): void
    {
        $this->flashMessage('Úspěšně přihlášeno', 'success');
        $this->redirect(':App:Dashboard:default');
    }
}

Presenter:

<?php declare(strict_types=1);

namespace App\SignModule\Presenters;

use Nette\Application\UI\Presenter;
use App\Components\Sign\In\PresenterTrait as SignInPresenterTrait; // Opět může být více trait v presenteru, tak je aliasuju  unikátním namespacem

// Ideálně jednoakční presenter, abychom nemuseli v traitách řešit přístup z jiné akce
class SignInPresenter extends Presenter // nebo BasePresenter
{
	use SignInPresenterTrait;
}

presenterovská šablona

{block content}
	...
	{control signInFormControl}
	...
{/block}

A takže už ve všech Presenterech používám tu traitu, která ten boilerplate kód co tvoří ty komponenty prostě do těch presenterů, kde to chci nakopírujou.

EDIT

Napadá mě, že použijeme-li pro vložení komponenty do presenteru traitu, tak Factory komponenty ztrácí smysl. Je to tak ??

Určitě ne. Ta továrna totiž není jen na oko, nebo jen nějaké odstínění psaní tvorby věcí, ale má hned 2 výhody.

  1. Díky továrně zaregistrované v DIC nám DIC do dané komponenty automaticky předá další závislosti a nemusíme je předávat ručně z Presenteru, kde je v té traitě budem muset stejně z DIC natáhnout
  2. Jelikož nebudeš mít slovíčko new v traitách, tak nebudeš mít natvrdo svázané presentery s těmi komponentami a tedy si zlepšíš testovatelnost aplikace.

Navíc je možné Control a ControlFactory v určitých případech zjednodušit podobně, jak se používá třeba datagrid. Tedy že pro všechny takto jednoduché třídy budeš mít jednu globální a měnit se bude jen šablona, FormFactory a Traita, což se hodí, protože těchto jednoduchých formulářů je většina

Editoval Bulldog (17. 10. 2022 3:04)

Bulldog
Člen | 110
+
0
-

m.brecher napsal(a):

@KamilValenta K layoutům bych podotkl, že hodně záleží na tom, jak vlastně layout stavíš. Pokud layoutem řešíš třeba nějaké části menu v administraci, tak potom souhlasím, že v jednom modulu může být více layoutů. Moduly jsou o seskupení souvisejících presenterů a komponent ideálně se stejnými uživatelskými právy. Layout nemusí nutně moduly kopírovat. Kvůli více layoutům myslím nestojí za to vytvářet další vrstvu v hierarchii presenterů. Tam traity asi splní svůj účel perfektně – nebo komponenty. Layout lze pojmout i jako komponentu.

V Nette moc projektů za sebou nemám, ale myslím, že koncept, že má administrace jeden hlavní layout a několik podlayoutů je u složitějších aplikací dobrý koncept. Až to budu někdy dělat, vzpomenu si ta traity ;).

Právě menu řeším komponentou, která se mění podle potřeby. Takže layout vypadá stejně i v rámci změny menu.

Ohledně přehlednosti změny layoutu mi přijde rozumnější například takováto struktura:

	BackModule	// Pouze pro přihlášené
		Components // komponenty obecně pro přihlášené uživatele (například výpis z účtu)
		Presenters // Zde je BasePresenter, který ověřuje, jestli je uživatel přihlášen a případně přesměruje na přihlášení

		SupervisorModule	// Pro supervisory s jejich vlastním layoutem/vzhledem stránky + možností se přepnout na uživatele (nemusí být v layoutu if)
			Components	// komponenty specifické pro supervisory (například možnost vidět seznam uživatelů)
			Presenters  // Presentery (včetně BasePresenteru ověřujícího, jestli jsme Supervisor a máme sem přístup)

		ClientModule	// Pro klienty s jejich vlastním vzhledem aplikace
			Components	// komponenty specifické pro klienty (například nákup předplatného, který supervisor nepotřebuje)
			Presenters // asi víme

Než takováto:

	BackModule	// Pouze pro přihlášené
		Components // komponenty pro všechny uživatele včetně těch konkrétních
		Presenters // Zde není BasePresenter, který ověřuje, jestli je uživatel přihlášen a případně přesměruje na přihlášení
		Traits	// Zde je traita oveřující přihlášení, traita ověřující, jestli jsi supervisor, traita ověřující jestli jsi zákazník
				// a všechny tyto traity jsou v různých presenterech použity různě, takže aniž by jsi rozkliknul daný presenter
				// tak nevíš, jestli ten presenter je pro klienta, nebo je pro supervisora, nevíš ani jaký layout zrovna používá
				// nevíš jaké je zamýšlené chování atp.

Samozřejmě druhá možnost se dá s tím, že nevíme který presenter co používá a jaký má layout atp. vyřešit správným názvoslovím, ale názvy pro presenter typu
ClientArticleAddPresenter
ClientArticleEditPresenter
ClientArticleListPresenter
ClientArticleDetailPresenter
ClientAccountStatementViewPresenter
ClientAccountNumberAddPresenter
ClientAccountNumberEditPresenter
ClientAccountNumberListPresenter
SupervisorArticleAddPresenter
SupervisorArticleEditPresenter
SupervisorArticleListPresenter
SupervisorArticleDetailPresenter
SupervisorAccountStatementViewPresenter
SupervisorAccountNumberAddPresenter
SupervisorAccountNumberEditPresenter
SupervisorAccountNumberListPresenter
SupervisorClientEditPresenter
SupervisorClientAddPresenter
SupervisorClientListPresenter
ClientSupervisorDetailPresenter

Atp. v rámci jednoho modulu (a to si vezměme, že toto je jen 5 logických stránek. Běžně jich weby mívají i 30…), nebo to ještě rozházené do více modulů, jak jeden kolega kdysi měl na každý jednotlivý Presenter samostatný modul třeba mi přijde zbytečně komplikované. Ztratil jsem se dokonce i když jsem to tu vypisoval.
Nejhorší jsou poslední 2. Je poslední opravdu presenter, kdy klient může nahlížet na detail svého supervisora, nebo jde o špatně nazvaný Presenter, kdy supervisor může nahlížet na detail svého klienta, když Presenter s názvem SupervisorClientDetailPresenter neexistuje? Jde o Presentery které používají layout od klienta, nebo supervisora? Mám se řídit zbylým názvoslovím, nebo byl kolega co to psal trochu mimo a nazval to špatně?

Toto s mým přístupem ‚modul = layout‘ neřešíš a problémy odpadají.

Samozřejmě nevím, jak to řeší ostatní, takže nemůžu říct, jak moc dobrý můj systém je. Nicméně z mého osobního pohledu a názoru je dobrý a nezaznamenal jsem zatím situaci, kdy by nešel použít.

Budu ale rád za jiné návrhy uspořádání. Nicméně to asi zase v novém vlákně?

Editoval Bulldog (17. 10. 2022 2:44)

m.brecher
Generous Backer | 758
+
+1
-

@Bulldog nemyslím, že by se mělo dělat nové vlákno, tato zajímavá diskuse je o architektuře projektu a tam ty věci navzájem všechny souvisí.

Teď už je dost pozdě, abych napsal něco delšího, ale dost jsem nad organizací projektů a názvoslovím přemýšlel. Protože u velkých projektů se člověk jednoduše může ztratit. U rozsáhlejších projektů je nezbytné projekt rozčlenit na jednotlivé moduly. Jinak to začne být nepřehledné. Zkoušel jsem ve své historii všechny možné varianty organizace souborů v projektu. Od verze všechny šablony v jednom adresáři s názvem presenter + akce Article.list.php, vkládané podšablony použité v jednom presenteru Article_sub.submenu.php, šablony v jedné akci Article.list.dataTable.php, teď zkouším systém /Article/list.latte.

V této chvíli jsem skončil u systému:

/app
	/Admin
		/Components			# komponenty modulu admin
			/templates
		/Forms				# formuláře modulu admin
			/templates
		/Presenters
			AdminPresenter.php  # předek modulu admin
			ArticlePresenter.php  # presentery odpovídají modelové jednotce entita - tedy tabulka, nebo tabulka + položky
			.......
		/templates	# šablony akcí presenterů + layout + doplňky layoutu
			/Article	šablony akcí ArticlePresenter
				default.latte
				update.latte
				create.latte
			.......
			@admin-layout.latte  - rozšiřuje @layout.latte
			... další šablony pro celý modul admin
	/Front
		/Components			# komponenty modulu front
			/templates
		/Forms				# formuláře modulu front
			/templates
		/Presenters
			FrontPresenter  # předek modulu front
			.......
		/templates	# šablony akcí presenterů + layout + doplňky layoutu
			/Article	šablony akcí ArticlePresenter
			.......
			@front-layout.latte  - rozšiřuje @layout.latte
			... další šablony pro celý modul front
	/Presenters
		BasePresenter   # předek modulových předků
		ErrorPresenter   # error presentery pro celý web
		Error4xxPresenter
		SmartPresenter	# traita vylepšující funkcionalitu Nette Presenteru
	/templates
		/Error
			šablony error presenteru
		@layout.latte	# base layout pro celý web
		flashes.latte	# flash okénko pro celý web
		.........
	/Model
		/Admin
			.....
		/Front
			.....
	/Forms			# moje vylepšeníčka Nette formulářů pro celý projekt
		DateInput.php
		DatetimeInput.php
		FormFactory.php
		SmartForm.php
		SmartFormControl.php
		SmartFormFactory.php
		/templates
			smartFormTemplate.latte
	/Components
		/templates
		..... # zde mám všechny komponenty pro layout

Tuto strukturu jsem navrhnul teprve nedávno. Daleko více by mě vyhovoval systém, kde by související presentery a šablony byly pohromadě v presenterové složce, ale co jsem nesnesl bylo to, že IDE řadí soubory podle abecedy a presentery byly někde uprostřed šablon. Tak jsem to opustil.

Jinak jak to mám navržené tak jsou názvy presenterů jednoduché a velmi čitelné, akce jsou všude víceméně stejné. Presenter je sice trochu složitější, ale celkově je to přehlednější.

Model:
co nejvíce rozčleněný a jednotlivé modelové třídy navzájem nezávislé.

Snažím se, aby to bylo podle kopyta: databázová tabulka má svoji modelovou třídu a tu řídí její presenter

takže např:
tabulka article – ArticleModel – ArticlePresenter, vše umístěno ve složce /Front

Teď dost řeším jak konkrétně postavit modelovou třídu, aby byla pro presenter a formuláře v komponentách co nejpohodlnější. Zatím jsem ve fázi hledání, a nějaké kódy sem kdyžtak zítra pošlu.

Kamil Valenta
Člen | 762
+
0
-

m.brecher napsal(a):

/app
	/Admin
		/Components			# komponenty modulu admin
			/templates
		/Forms				# formuláře modulu admin
			/templates
		/Presenters
			AdminPresenter.php  # předek modulu admin
			ArticlePresenter.php  # presentery odpovídají modelové jednotce entita - tedy tabulka, nebo tabulka + položky
			.......
		/templates	# šablony akcí presenterů + layout + doplňky layoutu
			/Article	šablony akcí ArticlePresenter
				default.latte
				update.latte
				create.latte
			.......
			@admin-layout.latte  - rozšiřuje @layout.latte
			... další šablony pro celý modul admin
	/Front
		/Components			# komponenty modulu front
			/templates
		/Forms				# formuláře modulu front
			/templates
		/Presenters
			FrontPresenter  # předek modulu front
			.......
		/templates	# šablony akcí presenterů + layout + doplňky layoutu
			/Article	šablony akcí ArticlePresenter
			.......
			@front-layout.latte  - rozšiřuje @layout.latte
			... další šablony pro celý modul front
	/Presenters
		BasePresenter   # předek modulových předků
		ErrorPresenter   # error presentery pro celý web
		Error4xxPresenter
		SmartPresenter	# traita vylepšující funkcionalitu Nette Presenteru
	/templates
		/Error
			šablony error presenteru
		@layout.latte	# base layout pro celý web
		flashes.latte	# flash okénko pro celý web
		.........
	/Model
		/Admin
			.....
		/Front
			.....
	/Forms			# moje vylepšeníčka Nette formulářů pro celý projekt
		DateInput.php
		DatetimeInput.php
		FormFactory.php
		SmartForm.php
		SmartFormControl.php
		SmartFormFactory.php
		/templates
			smartFormTemplate.latte
	/Components
		/templates
		..... # zde mám všechny komponenty pro layout

Tohle je přesně struktura, od které jsme před lety upustili a ani jeden den jsme změny nelitovali.
Nikdy jsem nepochopil, proč někdo dělá adminModule, je to asi pozůstatek z dob, kdy „admin skripty“ byly v podadresáři „admin“.

Z mého pohledu je modul soustava funkcí, která nad společným tématem pracuje samostatně. Proto chci, aby celý modul byl v jednom adresáři. Chce klient články? Zkopíruju jeden adresář articleModule. Chce eshop? Zkopíruju eshopModule. Každý modul v sobě má frontendové presentery, administrační presentery, neon configy, routy, modely, atp.

Jak bys teď v uvedeném příkladu řešil přenositelnost modulu Article? Vykopíruješ Front/Presenters/ArticlePresenter a šablony. Zkusíš spustit – 500. Aha, zapomněl jsi, že v admin části k tomu něco leží. Vykopíruješ Admin/Presenters/ArticlePresenter a šablony. Zkusíš spustit – 500. Aha, v adminu je krom ArticlePresenter ještě ArticleFeedPresenter. Dokopíruješ ještě tohle. Spustíš – zase 500. Člověk rozladěně kouká na Tracy, že to nenašlo model. No jo, ještě v úplně jiném místě k tomu měly být modely.

Já prostě nevidím přínos v tom mít všechny admin presentery u sebe, všechny front presentery u sebe, všechny modely u sebe, komponenty atp. Vidím přínos, když modul kompaktně má u sebe vše co potřebuje, hlouběji členěné na presentery, komponenty atp. Proto ta potřeba mít více layoutů v jednom modulu, jasně, protože frontové presentery jedou v jiném designu než administrátorské presentery.

Ten admin presenter v modulu si prostě jen traitou řekne o přihlášeného uživatele jménem a heslem a o vykreslení v admin layoutu. Jiný admin presenter (třeba z HW terminálu) si řekne o přihlášení tokenem a o vykreslení v terminálovém layoutu. Frontový presenter si řekne o front layout, eshopový presenter se zbožím si řekne o eshop layout, eshopový presenter s objednávkami si řekne o přihlášeného uživatele a eshop layout.

Ale jak jsem uvedl už včera v původním vlákně, s dědičností jsi uvedl funkční příklad, od počátku to nerozporuji. Jen byl označen za ideální a já chtěl na jeho nedostatky upozornit a nabídnout jinou cestu. Jak už to tak bývá, cest je vždy více a záleží na programátorovi, po čem sáhne. Dědičnost je super věc a strašně často ji používáme, ale futrování všeho společného do BasePresenteru je podobný přežitek, jako classa „App\Konstanty“ :)

Editoval Kamil Valenta (17. 10. 2022 14:19)

Bulldog
Člen | 110
+
0
-

@KamilValenta
Upozornění Bude delší příspěvek.

Velice hezky napsané. Pokud se nepletu, tak představuješ tento způsob popsaný už na Nette fóru, ale nenašel jsem odkaz:

App
	ArticleModule
		Presenters
			AdminArticleAddPresenter
			AdminArticleEditPresenter
			AdminArticleDetailPresenter
			AdminArticleListPresenter
			ClientArticleDetailPresenter	# je třeba aby byl jiný než AdminArticleDetailPresenter kvůli právům a jiné šabloně, pokud nechceme ifovat v každém presenteru...
			ClientArticleListPresenter
		templates
			adminArticleAdd.default.latte
			adminArticleEdit.default.latte
			adminArticleDetail.default.latte
			adminArticleList.default.latte
			clientArticleDetail.default.latte	# musí být jiný než adminArticleDetail.default.latte, protože administrátorská část většinou vypadá jinak, než klientská
			clientArticleList.default.latte
		Model
			Db
				Article.php					# ?? Správně?
				ArticleRepository.php		# ?? Správně?
			Security
				AuthorizatorFactory.php		# Konkrétní authorizátor pro přístup ke článkům?
		Router
			RouterFactory.php				# Router definující routy pro tento modul? (/admin/article/add, /admin/article/edit/<id>, /article/drtail/<id> atd.)
		# další specifické věci pro tento modul aby se mohlo ctrl+c a ctrl+v přesunout modul do dalšího projektu?
	# Další modul pro další logickou stránku?

Pokud ano, tak jsem jak jsem to tehdy četl byl nadšený z tohoto nového přístupu k věci a ihned jsem to vyzkoušel, ale postupem jsem od toho zase upustil, protože jsem narazil na spousty problémů, které jsem nevěděl jak řešit a můj způsob modul = layout je řeší hezky.
Popíšu některé z nich a prosím o doplnění, jak to řešíš v rámci tvého způsobu. Bylo mi líto totiž to řešení zavrhnout. Líbí se mi, ale z nutnosti jsem ho použít nemohl. Zkusím v otázkách i nastínit, jak jsem nad tím tehdy přemýšlel já a proč jsem to tedy zavrhl.

  1. Jak řešíš právě @layout.latte soubory? Protože řekněme, že máme 4 části stránky a tedy 4 layouty, jelikož každá část vypadá úplně jinak a to Admin, která má například jen textové tabulky s věcmi, nějaké grafy prodejů a formuláře na editaci bez nějakých extra grafických prvků. Pak máme Login část, která má grafický vzhled jako stránka, která je designovaná jen na jeden formulář a obchodní podmínky. Dále Client část designovanou příjemně s hromadou grafů a barev aby klienti se rádi vraceli. A nakonec Supervisor část, která je pro zaměstnance, je okleštěná od rozptylujících grafických prvků jako Admin část, ale zároveň není tak jednoduchá jako Admin část, aby se zaměstnancům lépe pracovalo, takže formuláře mají nějaký přidaný design například. Jak v tomto případě řešíš layouty? Kopírovat je do každého modulu nedává smysl. To by byla duplicita jako blázen. Máš je někde vně ArticleModulu? Jestli ano načítáš je v presenteru? Jestli ano máš na to Traitu, kterou kopíruješ do všech presenterů s prefixem daného layoutu? Pokud ano, neprotiřečí si tvůj argument s přenositelností, protože musíš pamatovat, aby jsi k tomu zkopíroval ještě traity z vně toho modulu? Pokud ne, dáváš je do všech šablon přes Latte tag {layout 'layout.latte'}? Pokud ano, kde vezmeš cestu k tomu layoutu? Dáváš ji tam natvrdo přes relativní cestu? Pokud ano, tak opět jak řešíš přenositelnost modulu, aby ti při přesunu do jiného projektu a nedej bůh jiné struktury, kterou jiný projekt používá layout fungoval? Nebo musíš pamatovat na to, aby jsi ten řádek projekt od projektu přepisoval na správnou cestu?
  2. Jak řešíš intersection modelové vrstvy? Řekněme, že v rámci článků budeme chtít mít možnost jako autoři ke článku přidat kontrolora z kolegů, který bude kontrolovat pravopis. Takže najednou v ArticleModule potřebujeme pracovat s UserRepository, ale ten je již nadefinovaný v UserModule. Co s tím? Mám UserRepository vytáhnout vně modulů a při znovupoužitelnosti pamatovat na to, že to musím zkopírovat taky? Nebo ten UserRepository s metodou getColleagues budu mít prostě v obou modulech a zapomenu na DRY princip a při úpravě fungování budu potřebovat to změnit na 2 místech? Nebo mám použít traitu, která v sobě bude implementovat metodu getColleagues a tato traita bude v obou UserRepository a sama bude umístěná vně Modulů a opět budu muset pamatovat, že ji musím při znovupoužitelnosti vzít s sebou?
  3. Jak řešíš definování Rout? Běžně používám něco jako:
public static function createRouter(): RouteList
{
	$translations = [		// klidně načteno z databáze, ale platí to pro aktuální modul
            'cs' => [
                'Prispeveky' => [
                    'presenter' => 'Blog',
                    'action' => [
                        'detail' => 'Detail',
                        'vypis' => 'Grid',
                    ],
                ],
                // Dalsi presenter
            ],
            'sk' => [
                'Prispevky' => [
                    'presenter' => 'Blog',
                    'action' => [
                        'detail' => 'Detail',
                        'vypis' => 'Grid',
                    ],
                ],
                // Dalsi presenter
            ],
            'en' => [
                'Blog' => [
                    'presenter' => 'Blog',
                    'action' => [
                        'detail' => 'Detail',
                        'list' => 'Grid',
                    ],
                ],
                // Dalsi presenter
            ],
        ];

        $router = new RouteList;

        $router->withModule('Admin')    // Pro celý modul nastavuji zde na jednom místě o jaký modul se jedná
            ->withPath('admin')           // a v případě nutnosti odlišit prefixem určitou část to udělám opět na jednom místě
            // Když definuju statické routy na nějaké stránky, které se nazývají ve všech jazycích stejně
            ->addRoute('/[<lang=cs cs|sk|en>/]robot', 'Robot:default')
            // a pak dynamické routy s překlady
            ->addRoute('/[<lang=cs cs|sk|en>/]<presenter>/<action>[/<id>]', [
                'presenter' => 'Homepage',
                'action' => 'default',
                'id' => null,
                null => [
                    Route::FILTER_IN => function (array $params) use ($translations): ?array {
                        // Filtr podle $translations
                        return ...;
                    },
                    Route::FILTER_OUT => function (array $params) use ($translations): ?array {
                        // Filtr podle $translations
                        return ...;
                    },
                ],
            ]);
    return $router;
}

A mám to pro celý modul jednotné a prefix definuji na jednom místě.
Pokud jsem ale použil tvé řešení, tak jsem buď měl rout spousty, kde se mi opakoval kód, nebo jsem měl routu vně (což by nebyl problém, protože zrovna routy se mohou měnit projekt od projektu), ale ta zase byla tak nabobtnalá, že byla nepřehledná a musel jsem se strašně štvát s tím, že co pár řádků muselo být withModule a withPath, což se mi častokrát hodně opakovalo a dělalo to router nepřehledným.

  1. Jak řešíš komponenty? Já mám například komponentu, která se stará o vykreslování seznamu článků a tu v rámci modulu/více modulů používám na více místech. Například chci seznam článků vykreslit ne jen v rámci Article části a tedy v presenterech ArticleModule\AdminArticleListPresenter a ArticleModule\ClientArticleListPresenter, ale i v rámci nějakého ‚náhledu‘ na nástěnce, takže ho potřebuji použít i v DashboardModule\ClientPresenter a DashboardModule\AdminPresenter. Kam tedy mám dát komponentu starající se o vykreslení toho seznamu článků? Pokud to bude v rámci ArticleModule, tak potom ztrácí na znovupoužitelnosti DashboardModule a obrácneně. No a pokud to bude vně modulů, tak jsme se zase dostali k tomu, jak jsi psal Aha, zapomněl jsi, že v admin části k tomu něco leží., akorát teď to není v admin části ale někde jinde ne?
  2. Jak řešíš ostaní funkčnosti, které mají být sdílené napříč presentery různých modulů? Například chceš-li vykreslit komponentu s kontaktním formulářem v patičce napříč všemi stránkami. Kam uložíš Traitu o to se starající, aby se zachovala znovupoužitelnost bez zapomínání, kde k tomu co leží?
  3. Jak řešíš specifické části stránky pro konkrétní typ role? Když nemáš nic jako AdminModul, ale potřebuješ všem adminům v rámci jejich layoutu povolit náhled na jejich podřízené a někde v tom layoutu vykreslit formulář se selectem na přepnutí? Opět nějaká traita vně modulů a do některých to dáš a do některých ne, místo společného předka v rámci celého jednoho modulu?
Bulldog
Člen | 110
+
0
-

To bylo pár dotazů. Problémů nastalo samozřejmě více, kdy jsem nevěděl, jak to pořádně strukturovat v projektu a výsledek byl, že byla obrovská základní složka, kde se objevovalo spousty komponent/tříd/služeb sdílených napříč jednotlivými moduly, ale jen některými zde, některými tam atp. No a tento návrh adresářové struktury, kdy mám kvůli 2 modulům, které sdílí stejné zdroje tyto zdroje vytaženy ven, i když jsou specifické jen pro Admin část mi přišlo v logickém měřítku stejné a tedy i stejně špatné, jako když parent třída má funkce, které potomek nevyužívá a z toho jsem odvodil, že to je špatný návrh.

Nyní to řeším tedy podobně, jak psal @mbrecher s tím rozdílem, že každá logická věc na stránce, v šabloně, má vlastní komponentu a tyto komponenty pomocí trait používám v presenterech, ke kterým toto logicky přísluší podle šablony.

Například pokud mám tedy výpis článků, kontaktní formulář v patičce a formulář na přepnutí na podřízeného, který je specifický pro layout, ke kterému je přiřazen, tak moje struktura vypadá asi takto:

App
	Components		# komponenty využitelné napříč moduly
		Article
			Grid
				Control.php			# stará se o vykreslení gridu - tahá data z databáze a předává je šabloně i s paginátorem - taktéž využívá ArticleRepository a UserRepository
				ControlFactory.php	# Interface pro tvorbu Control instance
				PresenterTrait.php	# Stará se o integraci do Presenteru a vyžaduje od presenteru, aby nastavil cestu k šabloně
		Contact
			Form
				Control.php			# opět vykreslení + tvorba formuláře z továrny
				ControlFactory.php	# jako výše
				FormFactory.php		# tvoří formulář a jako závislosti má mailer, kterému to pošle
				PresenterTrait.php	# jako výše
	AdminModule
		Components
			ArticleGridPresenterTrait.php	# dědí z 'App\Components\Article\Grid\PresenterTrait.php' a přepisuje cestu k šabloně
			ContactFormPresenterTrait.php	# dědí z 'App\Components\Contact\Form\PresenterTrait.php' a přepisuje cestu k šabloně
			Subordinate		# Specifická komponenta pro AdminModule, jinde se nepoužívá, protože v šablonách není (nikdo jiný než admin nemůže přepínat uživatele) a taktéž využívá UserRepository, stejně jako ArticleGrid
				Change
					Control.php			# opět vykreslení + tvorba formuláře z továrny
					ControlFactory.php	# jako výše
					FormFactory.php		# tvoří formulář a jako závislosti má model, který se postará o vyhodnocení
					PresenterTrait.php	# jako výše ale včetně nastavení šablony, jelikož nikdo jiný nebude potřebovat šablonu nastavit
			templates
				articleGrid.latte		# šablona specifická pro danou komponentu v rámci admin layoutu (protože jsme v admin modulu a zde bude tedy vypadat jinak, než v client modulu)
				contactForm.latte
				subordinateChange.latte
		Presenters
			Presenter.php		# abstraktní presenter ověřující práva na přístup pro adminy a využívající traity 'App\AdminModule\Components\ContactFormPresenterTrait' a 'App\AdminModule\Components\Subordinate\Change\PresenterTrait', jelikož tyto 2 komponenty se vykreslují v rámci AdminLayoutu.
			ArticleGridPresenter.php		# Konkrétní presenter pro výpis článků, který extenduje abstraktní presenter stejné složky a používá navíc traitu 'App\AdminModule\Components\ArticleGridPresenterTrait'
		templates 		# šablony pro presentery
			ArticleGrid.default.latte	# obsahuje vykreslení komponenty z traity pomocí makra 'control'
			@layout.latte				# obsahuje načtení obecných skriptů, základní vzhled stránky pro adminy, vykreslení obecných komponent z abstraktního presenteru atp.
		Router
			RouterFactory.php		# Definuje všechny routy pro daný modul a všem dá případný jednotný prefix na 1 místě
	ClientModule
		Components
			ArticleGridPresenterTrait.php	# dědí z 'App\Components\Article\Grid\PresenterTrait.php' a přepisuje cestu k šabloně
			ContactFormPresenterTrait.php	# dědí z 'App\Components\Contact\Form\PresenterTrait.php' a přepisuje cestu k šabloně
			# Tento modul nemá žádné své specifické komponenty zatím
			templates
				articleGrid.latte		# šablona specifická pro danou komponentu v rámci client layoutu (protože jsme v client modulu a zde bude komponenta vypadat jinak, než v admin modulu)
				contactForm.latte
		Presenters
			Presenter.php		# abstraktní presenter ověřující práva na přístup pro klienty a využívající traity 'App\ClientModule\Components\ContactFormPresenterTrait' a 'App\ClientModule\Components\Subordinate\Change\PresenterTrait', jelikož tyto 2 komponenty se vykreslují v rámci ClientLayoutu.
			ArticleGridPresenter.php		# Konkrétní presenter pro výpis článků, který extenduje abstraktní presenter stejné složky
		templates 		# šablony pro presentery
			ArticleGrid.default.latte	# obsahuje vykreslení komponenty z traity pomocí makra 'control', ale struktura je jiná, než ve stejnojmenné šabloně z admin modulu.
			@layout.latte				# obsahuje načtení obecných skriptů, základní vzhled stránky pro klienty, vykreslení obecných komponent z abstraktního presenteru atp.
		Router
			RouterFactory.php		# Definuje všechny routy pro daný modul a všem dá případný jednotný prefix na 1 místě
	Model	# model mám vždy obecně, protože se mi neoplatilo ho kouskovat do jednotlivých modulů a to z důvodů, že Presentery s ním vůbec nepracují.
			# Kdo s Modelem pracuje jsou komponenty a ty si vždy natáhnou jen ty modelové třídy, které potřebují.
			# Taktéž mi přišlo nelogické kouskovat například 'ArticleRepository' na 3 repa, kdy jedno umí jen načtení více článků, druhý umí jen načtení jednoho článku podle ID, třetí umí článek upravovat, další mazat, další přidávat atp.
			# někteří to obhajují principem SingleResponsibility, ale zrovna u repositářů si myslím, že single responsibility je i s tímto přístupem zachováno.
		Db
			Repo
				ArticleRepository.php	# Asi víme
				UserRepository.php		# nápodobně
			Entity
				Article.php
				User.php
		Api
			Email
				...
			Sms
				...

Teď se podíváme, jak vypadají jednotlivé soubory uvnitř AdminModulu
App\AdminModule\Presenters\Presenter.php:

class Presenter extends Nette\Application\Ui\Presenter
{
	use App\AdminModule\Components\ContactFormPresenterTrait;
	use App\AdminModule\Components\Subordinate\Change\PresenterTrait;

	public function startup(): void
	{
		if (!$this->user->isAllowed('Admin', 'view')) {
			$this->error('Zamítnuto. Přihlašte se.', 401);
		}
		parent::startup();
	}
}

App\AdminModule\Presenters\ArticleGridPresenter.php:

class ArticleGridPresenter extends Presenter
{
	use App\AdminModule\Components\ArticleGridPresenterTrait;
}

App\AdminModule\Components\ContactFormPresenterTrait.php: (Může mít další nastavení… To, že nastavuji jen šablonu není podstatné. Obecně se může nastavovat cokoliv. Třeba adresa E-mailu kam se má odesílat dotaz pro jednotlivé sekce atp.)

trait ContactFormPresenterTrait {
	use App\Components\Contact\Form\PresenterTrait;

	private string $contactFormTemplate = __DIR__ . '/templates/contactForm.latte';
}

App\AdminModule\Components\ArticleGridPresenterTrait.php:

trait ArticleGridPresenterTrait {
	use App\Components\Article\Grid\PresenterTrait;

	private string $articleGridTemplate = __DIR__ . '/templates/articleGrid.latte';
}

@layout.latte:

<!DOCTYPE html>
<html>
<head>
	globalni styly, meta atp. pro adminy
</head>
<body>
	specifický vzhled layoutu pro Adminy
	<header>
		{control subordinateChangeFormControl}	{* Vím, že je přístupný, jelikož je v abstraktním presenteru a nezapomenu použít traitu*}
	</header>

	{include content}

	globalni skripty pro adminy

	<footer>
		{control contactFormControl}	{* Vím, že je přístupný, jelikož je v abstraktním presenteru a nezapomenu použít traitu*}
	</footer>
</body>
</html>

ArticleGrid.default.latte:

{block content}
	speciální prvky vzhledu pro adminy v šabloně

	{control articleGridControl}
{/block}

articleGrid.latte:

speciální vzhled gridu pro AdminModul.

contactForm.latte:

speciální vzhled kontaktního formuláře pro AdminModul, aby zapadl do adminModule patičky.
Bulldog
Člen | 110
+
0
-

Jak je vidět, tak soubory uvnitř jednotlivých modulů jsou hodně lightweight a vlastně presentery nedělají nic jiného, než že skládají šablonu z určitých komponent. (Což je také samotný princip MVC. Presentery mají propojovat model s šablonou. Takže v mém případě komponenty s konkrétními šablonami konkrétního modulu)
Tento celý princip znamená to, že Modelová vrstva a komponentový model jsou nezávislé na presenterech a konečném vykreslení. Respektive jediné, co dělají presentery je, že vezmou pro daný požadavek konkrétní komponenty, řeknou těm komponentám do jakých šablon se mají vykreslit v rámci jakého Scope (admin, klient, přihlášení) a zbytek práce je nezajímá.

A vzhledem k tomu, že u KAŽDÉHO projektu je vzhled jiný, jelikož firmy se chtějí odlišovat, tak se dynamicky mění jak struktura presenterů, tak šablony. Ale jelikož komponentám a modelu je šablona úplně jedno, tak jsou jak model, tak komponenty plně přenositelné napříč projekty a jediné, co se musí upravit jsou šablony, které se stejně vždy upravují podle přání zákazníka a presentery, kterým řekneš, jaké komponenty má do které šablony vykreslit.

Ale upřímně ruku na srdce, to se musí dělat vždy, protože některý zákazník v té šabloně prostě ten formulář chce a jiný zase ne, takže presentery znovupoužitelné nejsou nikdy, jelikož staví tu stránku z oněch komponent podle přání zákazníka. Nemůžeme zákazníkovi říct hele my už máme tady na jednom webu nakódovaný presenter tak, že jsou spojeny články s výpisem komentářů, tak to prostě bude na jedné stránce i když vy tam komentáře nechcete.

Proto mít zvlášť komponentu na články a zvlášť na její komentáře a spojovat je v presenteru podle použití na různých webech podle aktuálního vzhledu šablony mi přišlo jako lepší řešení, než se snažit natvrdo kódit presentery různých šablon dohromady kvůli znovupoužitelnosti, u čehož mi vyvstanou problémy viz výše a stejně je musím projekt od projektu upravovat.

Navíc, pokud nějaká komponenta je použita ve vícero modulech, ale nechceme jí nastavovat nic, protože se vždy vykreslí stejně (například ReCaptcha), tak je jednoduše možné ji nadefinovat výchozí šablonu přímo u ní v adresáři a tu v případě potřeby nahradit konkrétní šablonou odpovídající danému modulu.

EDIT
Jo a zapomněl jsem napsat ještě, že každý modul může jednoduše definovat i přístupová práva k jednotlivým Presenterům ve svém AuthorizatorFactory, který se bude starat jen o daný scope, takže může v AdminModule být nadefinována příatupová logika pro administrátory do jednotlivých sekcí, což je zase další velké plus, protože se ta daná logika daného autorizátoru stará jen o danou konkrétní roli, která je přiřazená danému scope/šabloně, takže AdminModul omezuje přístupy k AdminPresenterům, ale ani neví, že existují nějaké ClientPresentery atp.

To mi přijde jako mírná výhoda taky, jelikož pokud jsem to měl rozkouskováno podle logických prvků a ne podle layoutů, tak jsem narážel na to, že když přibyla nová role, tak jsem musel projít celý kód a všechny moduly a upravit je podle nové role.
V případě, že mi přibyla nová role/layout u mého příatupu, tak jsem prostě jen přidal nový modul starající se o tu danou část.

Tedy Moduly se mi starají o celý logický layout/roli v rámci toho layoutu a o logické prvky stránky se mi starají komponenty, které jsou přenositelné napříč projekty.
Což dává smysl, protože vzhled a omezení přístupu přenositelné nemusí být, jelikož se mění projekt od projektu.

Tzn. komponenty se o práva přístupu nestarají. Už presentery musí přístup zamítnout, pokud nemáme práva.

Pokud se nepletu, tak stejný princip používá na vývoj MAGENTO, kde každá logická část stránky je komponenta a tyto komponenty Controllery vlastně ‚lepí‘ dohromady do požadované šablony, takže komponenty jsou znovupoužitelné prvky (Model), kterým šablony a tedy vzhled (View) přižazuje daný (Controller), což je princip MVC.
Takže z toho pro mě konkrétně plyne, že modul, který sdružuje více Controllerů dohromady by je měl sdružovat podle daného SCOPE, do kterého jednotlivé VIEW patří, což je pro mě logicky layout.

Editoval Bulldog (18. 10. 2022 1:27)

Kamil Valenta
Člen | 762
+
0
-

@Bulldog Pokusím se nastínit odpovědi, ten druhý příspěvek už jsem jen prolétl očima. Ono obecně ta struktura vznikala asi rok a těžko ji popsat na pár řádků, nechci z fora dělat blog nebo jím nahradit večer prosezený u piva, kde je prostor to probrat detailně.

Ad 1. Layouty a šablony obecně se dohledávají trochu složitějším mechanismem. Vycházíme z myšlenky, že buď se dohledá šablona/layout obecná, sdílená mezi zákazníky (typicky u admin modulu), nebo se přednostně dohledá šablona privátní, pokud existuje (typicky frontové presentery). Ano, kvůli těmto věcem udržujeme baseModul, kde může být front layout, admin layout, neon s připojením k db apod.
Toto je sice trochu „vnější závislost“, ale připojení k db a nějaký vzhled chce mít každý projekt, takže ty soubory stejně existují a při kopírování modulů se nekopírují.
Pokud klient na něco vlastní šablonu nemá, deploy mu dodá tu obecnou.

Ad 2. model Article se prostě přiznává k volitelné závislosti na modelu User. To není potřeba vynášet o úroveň výš. Modul Article tak nabízí různým klientům různé možnosti, podle kombinací ostatních modulů.

Ad 3. Routování se řídí z db, takže prefixy a definice jazykových mutací je na jednom místě v jedné tabulce. A ta se cachuje, protože po spuštění projektu se téměř nemění.
Routy pak vypadají celkem čtivě:

public function createRouter()
{
    $router = new RouteList('Article');
    $router[] = new Route($this->routy->vratAdminRoutu().'/clanky[/<action>]', [
        'presenter' => 'Admin',
        'action' => 'default',
        'lang' => 1,
    ]);

    foreach ($this->routy->vratRouty() as $routa) {
        $router[] = new Route($routa->routa.'/clanek/<slug>', [
            'presenter' => 'Article',
            'action' => 'default',
            'lang' => $routa->lang,
        ]);
    }

Ad 4. Opět není problém přiznat nepodmíněnou závislost, komponenta leží fyzicky jen v modulu, do kterého patří, jiné moduly si požádají factory, pokud je cílový modul k dispozici. Když není, tak se komponenta nevykreslí (typicky na frontu), nebo se vykreslí hláška v duchu „aby toto bylo vidět, je potřeba modul X“ (typicky v admin)
Některé komponenty jsou zcela uzpůsobeny na sdílení svého obsahu v „boxíkách“, takové implementují IWidget (jo, držím u interfaců prefix „I“, no flame :) ), presentery pak s těmi Widgety umí pracovat ještě dál, klient si to může v administraci naklikávat atp.

Ad 5. Protože uvádíš příklad formuláře, tak to bude traita ve formModule. A každý presenter si o ni řekne. Ono to nebude „všude“, jak uvádíš. Admin presentery ji chtít nebudou, Popup okna/modaly ji chtít nebudou…
Trochu to na mne působí, že máš pocit, že sdílený obsah musí být v adresářové struktuře vždy výš, což nemusí.

Ad 6. Takové přepínání by bylo řešeno komponentou a traitou uvnitř userModule. A ano, každý admin presenter, který má podporu pro takové náhledy, se k tomu ve své „hlavičce“ přizná. To je právě ta výhoda, pracuješ s nějakým presenterem a vidíš, co umí. Nemusíš se proklikávat přes více úrovní dědičnosti a skládat v hlavě, co ve které vrstvě přibylo.

Editoval Kamil Valenta (18. 10. 2022 10:35)

Bulldog
Člen | 110
+
0
-

@KamilValenta
Díky za odpovědi. Je pravda, že toto téma je na velmi obsáhlé a vydá na spousty stran textu. Nezvažoval jsi o struktuře aplikací v Nette napsat třeba mini kurz na https://www.itnetwork.cz/ ? Rád bych si o tom něco nastudoval hlouběji, tak by mi takový kurz přišel vhod.

Nicméně s tvými odpověďmi mi přišlo více otázek, než odpovědí. Bohužel.

Hlavní myšlenka, která mě provází je:

Ad 2. model Article se prostě přiznává k volitelné závislosti na modelu User. To není potřeba vynášet o úroveň výš. Modul Article tak nabízí různým klientům různé možnosti, podle kombinací ostatních modulů.

Mám modul Article, kde je teda sada Presenterů, které umí pro jednotlivé vzhledy vykreslit články.
K tomu mám tedy modul User a modul Article má na něm volitelnou závislost.
Modul User je dělaný tak, že má v sobě vykreslení pro adminy, kteří mohou manipulovat se všemi klienty, pak má editaci klienta, přidání klienta, pak Sign část, kde je registrace, přihlášení, zapomenuté heslo a klientskou část, kde je osobní nastavení.

Tedy User modul se stará o vykreslení části 3 různých částí webu a to o Admin část, Client část a Sign část a má v sobě komponenty s názvy: ClientGrid, ClientManipulate(toto je pro editaci i přidání), ClientSignUp, ClientSignIn, ClientForgotPassword a PersonalSetting
Do tohoto bodu v pohodě.

Co když ale tedy přijde jiný projekt, kde budeš potřebovat články, ty mají volitelnou závislost, to je v pohodě a ty tedy přetáhneš ten User modul, ale na novém webu budeš potřebovat jen přihlášení a editaci článků, protože tam třeba zákazník vůbec nebude chtít administrátorskou část? A nebo když ji bude chtít, tak v ní nemusí chtít mít náhled na klienty a nemusí je chtít editovat.
Nebo co když u něj bude admin část něco jiného a tedy administrátoři budou mít jiná práva a to, co by dělali normálně administrátoři tak budou dělat sami klienti, jelikož budou mít nějakou hierarchii pod sebou?

Najednou se z mého pohledu drasticky mění struktura projektu i návrhu presenterů, protože admin modul vlastně nově je sloučen s UserModulem, takže najednou nepotřebuji na to 2 presentery, kdy to bude každý presentovat jinak, ale stačí jeden, takže se mi tahle moje struktura bude měnit projekt od projektu a to nepočítám to, že naprogramovat volitelné závislosti bývá často pain, protože musíš dělat spousty podmínek, jak se to má chovat když to tu závislost dostane a když ne.

Není pak lepší se na tu strukturu Presenterů vyprdnout, nechat všechnu logiku na komponentách a konkrétní komponenty, které jsou plně přenositelné mít jako cihly, ze kterých stavíš stránku dohromady a jsou nezávislé na tom, jak to postavíš?

Tedy v druhém případě řekneš ahá, takže tady si vezmu komponentu na přidání/editaci článků, tady na výpis článků a když budu chtít, tak i tady na seznam uživatelů atp. a z těch cihel (komponent) si vybereš jen ty komponenty co potřebuješ a Presenterovskou vrstvu vždy postavíš na míru danému projektu, což je v pohodě, když Presentery stejně mají max 10 řádků?

Mě se tohle třeba osvědčilo v tom, že ty komponenty si můžu narvat jako DI extension a tahat je jako závislosti přes composer. Takže mám fakt jakoby lego stavebnici kdy composerem natáhnu kostičky, které chci použít a jsou na sobě zcela nezávislé a Presentery jsou pro mě unikátní návod, jak z těch kostiček postavit daný web. Tedy Presentery popisují vzhled a chování webu, které buildí z těch komponent.

Editoval Bulldog (18. 10. 2022 18:01)

Kamil Valenta
Člen | 762
+
0
-

Bulldog napsal(a):
Nezvažoval jsi o struktuře aplikací v Nette napsat třeba mini kurz na https://www.itnetwork.cz/ ?

Popravdě nezvažoval. Kdysi v jednom vlákně fora David Grudl vyzval, jestli by o modulární struktuře někdo nenapsal do nette blogu, ale já do toho nechtěl jít sám. Jsme celkem malý tým a ta struktura nemá tak výraznou oponenturu. Samozřejmě vždy je něco za cenu něčeho, ale dělá se nám v tom enormně dobře a rychle.

Co když ale tedy přijde jiný projekt, kde budeš potřebovat články, ty mají volitelnou závislost, to je v pohodě a ty tedy přetáhneš ten User modul, ale na novém webu budeš potřebovat jen přihlášení a editaci článků, protože tam třeba zákazník vůbec nebude chtít administrátorskou část? A nebo když ji bude chtít, tak v ní nemusí chtít mít náhled na klienty a nemusí je chtít editovat.

A tohle je právě chvíle, kdy dědíme. Ono k tomu nedochází moc často, i těch provazeb mezi moduly není mnoho a když už jsou, tak většinou všem vyhovují. Pokud se to ale sejde a zákazník to chce jinak, získá podědený presenter (komponentu, model, neon, šablonu…) a získá úpravu na míru.

naprogramovat volitelné závislosti bývá často pain, protože musíš dělat spousty podmínek, jak se to má chovat když to tu závislost dostane a když ne.

Nastat to může, ale v praxi se s tím nesetkáváme. Většinou ta entita získá nějaký celý tab s nastavením.

Není pak lepší se na tu strukturu Presenterů vyprdnout, nechat všechnu logiku na komponentách a konkrétní komponenty, které jsou plně přenositelné mít jako cihly, ze kterých stavíš stránku dohromady a jsou nezávislé na tom, jak to postavíš?

Tuhle zkušenost mám z jiné firmy a bylo to dost šílené. Ale mohlo to být jen tamním pojetím těch komponent. Já bych ale logiku to komponent moc necpal.

Bulldog
Člen | 110
+
0
-

Díky za objasnění. Je pravda, že pokud se něco neděje moc často, tak nemá smysl to řešit dokud to nenastane.
Mám aspoň nad čím přemýšlet a co zkoušet. Díky.

m.brecher
Generous Backer | 758
+
0
-

@KamilValenta
>

Jak bys teď v uvedeném příkladu řešil přenositelnost modulu Article? Vykopíruješ Front/Presenters/ArticlePresenter a šablony. Zkusíš spustit – 500. Aha, zapomněl jsi, že v admin části k tomu něco leží. Vykopíruješ Admin/Presenters/ArticlePresenter a šablony. Zkusíš spustit – 500. Aha, v adminu je krom ArticlePresenter ještě ArticleFeedPresenter. Dokopíruješ ještě tohle. Spustíš – zase 500. Člověk rozladěně kouká na Tracy, že to nenašlo model. No jo, ještě v úplně jiném místě k tomu měly být modely.

V tom projektu co jsem poslal není víc presenterů než toto:

/Front
	HomepagePresenter
	ContentPresenter
	SignPresenter
	UserPresenter

/Admin
	HomepagePresenter
	SectionPresenter
	CategoryPresenter
	ArticlePresenter

/Model
	/Front
		.....
	/Admin
		.....

Víc toho tam první dva roky nebude. Moduly jsou 100% nezávislé jak presentery, tak šablony, css, modelové třídy. Jenom UserManagement se měl oddělit, to jsem byl líný.

Pokud se aplikace staví jako stavebnice bundlů, potom je v pořádku to členit podle distribuce. Vy v podstatě zřejmě tolik nevyvíjíte, ale distribujete hotovou aplikaci, dolaďujete a něco dopíšete na míru. Pak se se soubory pracuje jinak, než když se vyvíjí unikátní a neopakovatelná aplikace.

Pointa u organizace v projektu je ta, aby odpovídala tomu jak se se soubory pracuje. Při vývoji je ideální mít související soubory pohromadě – takže ideální členění vidím takhle:

Ideální členění

/app
	/Admin
		/Article
			ArticlePresenter.php
			ArticleForm.php
			ArticleFormFactory.php
			articleForm.latte
			create.latte
			update.latte

	/Model
		/Admin
			ArticleModel.php	model pro /Admin/Article/ArticlePresenter.php

Aby to bylo použitelné, muselo by IDE řadit soubory ve složkách presenterů podle typu ne abecedně. Vyzkoušel jsem to a když se míchaly šablony s presenterem pokaždé v jiném pořadí nebylo to bohužel pro mne použitelné. Šikovný plugin do IDE by to vyřešil.

Model ze známých důvodů je vhodné mít stranou – ovšem v projektech určených pro bundlovou distribuci jak máte ve firmě může být výhodné toto pravidlo porušit.

Určitě je dobré oddělit model veřejné části a administrace tak, aby pro to byly samostatné třídy – pro klienty, kde výrazně převažuje návštěvnost veřejné části nad administrací, což většinou tak je. Proč by se 1000 x tahal celý model včetně administrace Article, když z toho bude administračních requestů cca 2–3? Ale to tak asi máte?

Jednoakční presentery ( @Bulldog )? Mě to neláká, a) je to příliš velké rozdrobení souvisejících věcí na kousky, b) naroste počet souborů násobně, c) příliš dlouhé názvy presenterů, Výhody? Když v presenterech nebudou fyzicky formuláře ani komponenty, tak žádné velké výhody nevidím.

Ještě ke komponentám v traitách. Vezměme třeba formuláře. Já asi budu úplně všechny formuláře vytvářet přes factory, která je v samostatném souboru. V presenteru bude jenom jeden řádek pro získání služby factory a dva řádky pro zavolání factory. Oproti use trait to je o pár řádků více, rozdíl minimální. Čitelné to je, vím jaký formulář v presenteru mám, traita není lepší. U běžných formulářů je to asi skoro jedno co se použije – factory nebo traita.

Ale úplně rezignovat na BasePresenter?

Můj BasePresenter vypadá takhle:

<?php

declare(strict_types=1);

namespace App\Presenters;

use App\Components\LoginButton;
use App\Components\MainMenu;
use App\Components\TreePathMenu;
use App\Components\UserButton;
use App\Model\NavModel;
use App\Model\TreePathModel;
use App\Utils\Utils;
use Nette\Application\UI\Presenter;
use Nette\DI\Attributes\Inject;

abstract class BasePresenter extends Presenter
{
    use SmartNette;

    /* utils */

    #[Inject]
    public Utils $utils;

    public string $appDir;

    public bool $debugMode;

    /* layout */

    #[Inject]
    public NavModel $navModel;                // časem přesunu do komponenty

    #[Inject]
    public TreePathModel $treePathModel;     // časem přesunu do komponenty

    public function startup()
    {
        parent::startup();
        [$canonicalUrl, $httpCode] = $this->utils->getCanonUrl(redirectLocal: true);
        if($canonicalUrl){
            $this->redirectUrl($canonicalUrl, $httpCode);    // fix doc_root duplicity
        }
    }

    public function beforeRender()
    {
        $this->template->appDir = $this->appDir;
   }

    public function createComponentTreePathMenu(): TreePathMenu
    {
        return new TreePathMenu($this->treePathModel);
    }

    public function createComponentMainMenu(): MainMenu
    {
        return new MainMenu(
            navModel: $this->navModel,
            treePathModel: $this->treePathModel,
            isFrontModule: $this->isModuleCurrent('Front'),
        );
    }

    public function createComponentLoginButton(): LoginButton
    {
        return new LoginButton;
    }

    public function createComponentUserButton(): UserButton
    {
        return new UserButton;
    }
}

4 komponenty které se používají úplně v celém webu

$appDir aby se mohly efektivně vkládat šablony

fix doc_root duplicity přesměrováním

$debugMode po dobu vývoje

$utils na nějaké vychytávky, vždycky se něco najde

Prostě ten projekt od začátku stavím tak, že vím, že je pár věcí, které mám úplně všude, ve všech presenterech. Není to tak, že je využije jenom některý presenter. Věci co používá jen pár presenterů bych do BasePresenteru nedával, jsou jiné techniky jak to udělat – šablony, přidání v bloku šablony nebo naopak vyřazení bloku layoutu pro určité šablony.

Mám např. jeden formulář ve dvou presenterech – jednoduše ho registruji v těch dvou presenterech. Proč bych na to dělal předka – to by nedávalo smysl.

Je vidět, že jsou různé cesty a kdo je šikovný, využije z konkrétní techniky či technologie pro svůj specifický projekt to nejlepší. Různá doporučení a vzory jsou obecná a v konkrétním případě může být výhodné je otrocky nedodržovat.

Každopádně jsem za tuto pro mne osvětovou diskuzi velmi vděčný, protože mne obohatila. Člověk má takové klapky že vidí jen tu svoji cestu.

Bulldog
Člen | 110
+
0
-

@mbrecher

Proč by se 1000 x tahal celý model včetně administrace Article

Co tím myslíš? Nerozumím tomu, kde by se 1000× tahal nějaký model, který nepotřebuješ.
Pokud jde o třídu, že má v sobě funkce, které nepotřebuješ, tak to je jedno, protože počet funkcí nemá na rychlost vliv. PHP prostě ty funkce, co nevyužiješ ignoruje.

Jednoakční presentery

Já je taky nemám osobně rád a ze stejných důvodů. Co ale musíš mít na paměti je to, že jakékoliv komponenty a signály jsou přístupné v rámci celého Presenteru. Takže pokud máš například články, kdy může přidávat články jen uživatel, co má prémium a mazat je jen uživatel, co je vlastník, tak rozdíl mezi jednoakčními presentery a víceakčními je asi takový:

class ArticlePresenter extends Ui\Preseter
{
	#[Inject]
	public ArticleRepository $articleRepo;
	private Article $article;

	public function actionAdd(): void
	{
		if (!$this->user->isAllowed('Article', 'add')) {
			$this->error('Not allowed', 401);
		}
	}

	public function createComponentArticleAddForm(): Ui\Form
	{
		if (!$this->user->isAllowed('Article', 'add')) {
			$this->error('Not allowed', 401);
		}

		// definice formu
	}

	// Akce, ve které je možno mazat článek
	public function actionEdit(int $id): void
	{
		$article = $this->articleRepo->get($id);
		if (!$article) {
			$this->error('Not found', 404);
		}
		if (!$this->user->isAllowed($article, 'edit')) {
			$this->error('Not allowed', 401);
		}
		$this->article = $article;
	}

	public function createComponentArticleEditForm(): Ui\Form
	{
		if (!isset($this->article)) {
			// Musíme ověřit i existenci článku, aby web nepadal s chybou 500
			$this->error('Not found', 404);
		}
		if (!$this->user->isAllowed($this->article, 'edit')) {
			$this->error('Not allowed', 401);
		}

		// definice formu
	}

	public function handleDelete(): void
	{
		if (!isset($this->article)) {
			// Musíme ověřit i existenci článku, aby web nepadal s chybou 500
			$this->error('Not found', 404);
		}
		if (!$this->user->isAllowed($article, 'edit')) {
			$this->error('Not allowed', 401);
		}
		// Mazání a redrawControl/redirect..
	}

	// zbytek presenteru na definici dalších akcí
}

Vs jednoakční presentery článku

class ArticleAddPresenter extends Ui\Preseter
{
	public function startup(): void
	{
		parent::startup();
		if (!$this->user->isAllowed('Article', 'add')) {
			$this->error('Not allowed', 401);
		}
	}

	public function createComponentArticleAddForm(): Ui\Form
	{
		// definice formu teď už bez ověření, jelikož víme, že přes startup se nikdo bez práv nedostane
	}
}

class ArticleEditPresenter extends Ui\Preseter
{
	#[Inject]
	public ArticleRepository $articleRepo;
	private Article $article;

	public function startup(): void
	{
		parent::startup();
		$id = $this->getHttpRequest()->getQuery('id');

		$article = $this->articleRepo->get($id);
		if (!$article) {
			$this->error('Not found', 404);
		}
		if (!$this->user->isAllowed($article, 'edit')) {
			$this->error('Not allowed', 401);
		}
		$this->article = $article;
	}

	public function createComponentArticleEditForm(): Ui\Form
	{
		// definice formu teď už bez ověření, jelikož víme, že přes startup se nikdo bez práv nedostane
	}

	public function handleDelete(): void
	{
		// Mazání a redrawControl/redirect.. Zase bez ověření...
	}
}

Takže jsou takové zjednodušení, aby jsi nezapomněl na if, který zabrání bezpečnostní chybě a tedy použití jednoakčních presenterů je bezpečnější. Na oplátku se sice méně napíšeš, ale zase vzroste počet souborů. Podle mě je to programátor od programátora. Dokud to dodržuje stejně všude, tak ať to dělá jak chce.

Ještě ke komponentám v traitách. Vezměme třeba formuláře. Já asi budu úplně všechny formuláře vytvářet přes factory, která je v samostatném souboru. V presenteru bude jenom jeden řádek pro získání služby factory a dva řádky pro zavolání factory. Oproti use trait to je o pár řádků více, rozdíl minimální. Čitelné to je, vím jaký formulář v presenteru mám, traita není lepší.

Nesouhlasím. I kdyby vypadalo vytvoření formuláře, který se tvoří přes továrnu, v presenteru následovně:

	#[Inject]
	public SomeFormFactory $someFormFactory;

	public function createComponentSomeForm(): Form
	{
		return $this->someFormFactory->create();
	}

Tak je pořád lepší mít tento úryvek kódu v traitě a do Presenteru to usovat, protože:

  1. Presentery budou přehlednější
  2. Při použití ve více Presenterech dodržíš princip DRY
  3. Když se stane a budeš muset náhodou přidat nějaké nastavení, nebo udělat nějakou změnu (e.g. formulář najednou nebude jen na přidání, ale bude i na editaci a tedy bude potřebovat entitu s výchozími hodnotami, nebo budeš potřebuvat mu dynamicky měnit šablonu atp.), tak změny provedeš pouze v traitě a propíše se to do všech Presenterů, kde se to používá. V případě, že by jsi tento kód dával natvrdo do Presenterů, tak si zavařuješ na problémy, kdy budeš muset hledat, kde všude ten formulář používáš
  4. Tohle je asi největší výhoda. Traitu můžeš jednoduše narvat do mock třídy a tedy otestovat, jestli vše běží jak má, aniž by jsi musel testovat celý presenter:

traita:

trait SomeFormFactoryTrait
{
	#[Inject]
	public SomeFormFactory $someFormFactory;

	public function createComponentSomeForm(): Form
	{
		return $this->someFormFactory->create();
	}
}

presenter:

class SomePresenter extends Ui\Presenter
{
	use SomeFormFactoryTrait;
}

Testování:

class SomeFormFactoryTraitTest extends Tester\TestCase
{
	public function testOne()
	{
		$presenterMock = new class() {
			use SomeFormFactoryTrait;	// Do anonymní třídy (mocku) dám svou traitu, kterou chci otestovat.
		};
		$presenterMock->someFormFactory = // dodejme z DI, nebo pomocí new (tady můžem použít new. jsme v testech.)
		$component = $presenterMock->createComponentSomeForm();
		// aserce...
	}
}

Můj BasePresenter vypadá takhle:

Na předání appDir do všech šablon používám v configu tohle:

services:
	lattePropsHelper: App\Helpers\LatteDefaultProps(appDir: %appDir%)
	latte.templateFactory:
		setup:
			- "$onCreate[]" = [@lattePropsHelper, 'register']

a v LatteDefaultProps:

class LatteDefaultProps
{
    use SmartObject;

    public function __construct(
        private readonly string $appDir,
    ) { }

    public function register(\Nette\Bridges\ApplicationLatte\DefaultTemplate $template): void
    {
		// Tady definuju property šablony stejně, jak jsem zvyklý v Presenteru, ale je to na jednom místě pro všechny šablony, které vytvořím pomocí TemplateFactory
        $template->appDir = $this->appDir;
    }
}

Ohledně těch new v tom tvém Presenteru jen doporučení. Používej je s rozmyslem. Je to tvrdá závislost, snižuje testovatelnost a hlavně v případě, že přibydou tomu, co tvoříš závislosti, které jsou registrované v Configu, tak je tam budeš blbě dostávat. Doporučuju taky mít kód jednotně a vše dělat přes továrny. Není nic horšího, když je to někde tak a jinde jinak a pak místo toho, aby jsi s tím počítal, tak musíš dávat pozor, kde to je jak uděláno. Nette umí generované továrničky, takže předělat to takový pain není.

Editoval Bulldog (19. 10. 2022 11:11)

m.brecher
Generous Backer | 758
+
0
-

@Bulldog

Ahoj díky za skvělé komentáře k zamyšlení.

1000 x tahal

Samozřejmě vím, že se vykoná jen potřebná metoda, ale ten odkazovaný test nebere v úvahu čas na include malého/velkého souboru. Nicméně model pro Front nesdílí s modelem pro /Admin vůbec nic, protože /Front hledá podle slug, zatímco /Admin podle id. Nedává to logický smysl dávat dohromady věci co nesouvisejí. A je lepší mít model škálovaný podle presenter modulů, je to přehlednější.

bezpečnostní riziko komponent presenteru

Samozřejmě tohle vím, že lze autorizaci akce presenteru obejít a před rokem jsem na to na fóru upozorňoval. Dokonce ji lze obejít i přes NEEXISTUJÍCÍ akci, takže autorizovat akce nedává smysl. Proto pro mne základním architektonickým hlediskem je pokud to jde mít v presenteru všechny akce se stejnou úrovní autorizace (role). Nicméně platí, že best best practice je jednoznačně autorizace v modelu. Model je ten jediný skutečný strážce dat, best best je, aby autorizoval on. V jednodušších cms-kách se to dá zjednodušit na autorizaci v presenterech.

Zase se dostáváme k tomu, že záleží na celkové filozofii výstavby webu. Právě bezpečnost mě vede k tomu mít hierarchii presenterů, protože zde se nejlépe vytvoří základní zabezpečení vstupu pro přihlášené. A NIKDY nedávám do jednoho presenteru akce, kde by se úroveň autorizace lišila. Moje presentery jsou malé, obvykle tříakční – list, create, update vždy s konkrétní entitní tabulkou, pokud má entita položky, přibudou akce pro manipulaci s položkami addItem, updateIte, listItem. A při analýze jednoduše v návrhu rolí se snažím do tohoto schématu trefit. Samozřejmě někdy je to složitější, potom správným řešením je autorizovat v modelu.

Vkládání formulářů do presenterů

	#[Inject]
	public SomeFormFactory $someFormFactory;

	public function createComponentSomeForm(): Form
	{
		return $this->someFormFactory->create();
	}

Asi to bude hodně individuální, ale já preferuji vedle minimalizace kódu ještě čitelnost. A mě osobně vyhovuje princip střední cesty – co nejstručnější kód, ale ne do extrému. Pro mne je kód nahoře čitelnější než ho schovat do use. Ale zase zpět k mojí filozofii výstavby – v podstatě se mě nestává, že bych aplikaci navrhnul tak, aby vznikla potřeba mít formulář ve dvou/třech presenterech. S výjimkou SignUpForm, kde právě z důvodů security akcí presenteru registraci provede uživatel v presenteru SignPresenter veřejně přístupném a editaci registrace v presenteru UserPresenter pro přihlášené. Tak je to v souladu s mojí filozofií výstavby.

Takže když dobře analyzuješ a navrhuješ aplikaci, tak máš velkou většinu formulářů jen pro jeden presenter.

Jsou výjimky, kdy ve složitějších aplikacích se může kaskádově volat formulář pro související entitu k hlavní entitě, ale jen vyjímečně. Tam se prostě presenter k závislosti přizná.

mixovat styly?

Nakonec souhlasím, že use + úryvek kódu presenteru je dobrý nápad a nechť se využívá dle libosti. Co je důležité, je mít propracovaný systém který člověku vyhovuje, protože jedna věc nic neřeší. A jak píšeš lepší než střídat různé přístupy je držet se jednoho zvoleného stylu, protože pak člověk nemusí přemýšlet jak to v konkrétním presenteru vlastně udělal. Proto je použití trait v presenterech na vkládání formulářů i komponent v principu OK, ale ideálně vkládat traitami nebo kódem, ale nemíchat to.

nadbytečné bolierplate soubory

Co mě irituje, ale vím že to tak technicky musí být jsou nadbytečné soubory jako třeba soubory pro automatické generování factory pro formuláře/komponenty. Mám tak ke každému souboru s formulářem navíc jeden servisní s interface jenom proto, že to framework zatím neumí udělat jinak bez těch souborů.

A když umístíme každý formulář do komponenty a volání formuláře do traity, co se stane? Budeme mít o jeden v podstatě boilerplate soubor navíc:

ArticleFormControl.php
ArticleFormControlFactory.php   // boilerplate interface
ArticleFormControlTrait.php     // boilerplate volání factory + Injection

presenter bude čistší, ale za jakou cenu?

use ArticleFormControlTrait

Takže takhle používat traitu na vkládání formulářů nedává smysl. Traita pro formuláře dává smysl, pokud rovnou formulář v presenteru vytvoří a obejdeme se bez bolierplate souborů ArticleFormControl.php a ArticleFormControlFactory.php. Přijdeme o technické výhody které přináší obalení formuláře Control komponentou – možnost persistentních parametrů a oddělené vykreslování, bez toho se ale obvykle dá žít.

Takže mě dává smysl buďto všechny formuláře do presenterů traitou bez Control a bez Factory, nebo Nette doporučovaný způsob Control + Factory pomocí interface + createComponent… v presenteru. Tam je bohužel boilerplate interface factory, což by se asi dalo vyřešit ve frameworku.

obyčejný $appDir a tak složité to je

Díky za řešení injektování $appDir do šablon:

services:
	lattePropsHelper: App\Helpers\LatteDefaultProps(appDir: %appDir%)
	latte.templateFactory:
		setup:
			- "$onCreate[]" = [@lattePropsHelper, 'register']

Tvoje řešení jde rovnou k věci a injektuje rovnou do šablon. Bohužel je to složitý, nečitelný kód, přitom mít $appDir v šablonách je IMHO zásadní věc. V podstatě v každém projektu potřebujeme vkládat nějaké obecné šablony, které jsou mimo file scope dané šablony a tam je nejlepší použít absolutní cestu místo relativního posunu o x úrovní výše a zase dolů. Můj názor je, že tohle má řešit framework a že stejně jako je ve všech šablonách k dispozici $basePath, měl by tam být $appDir, aby si každý nemusel bastlit svoje komplikované řešení.

Editoval m.brecher (19. 10. 2022 15:35)

Bulldog
Člen | 110
+
0
-

nebere v úvahu čas na include malého/velkého souboru

Good point. Nasimuloval jsem si tedy lokálně include 1 malého souboru (115 znaků) a pak 1 velkého souboru (2185 znaků).
Pak jsem si pomocí Apache benchmarku zavolal 100 000× request a průměrné časy byly:
Malý soubor: Time per request: 1.305 [ms] (mean, across all concurrent requests)
Velký soubor: Time per request: 1.319 [ms] (mean, across all concurrent requests)

Z toho jsem usoudil, že asi mám málo souborů. Tak jsem si vytvořil skript, který mi vytvořil 20000 malých souborů (všechny ve stejné složce) a po testování jsem je smazal a vytvořil 20000 velkých souborů (taky všechny ve stejné složce)
Pak jsem si pomocí Apache benchmarku zavolal 20× (ano malé číslo, ale načítání souborů v rámci jedné složky travá prostě dlouho :D )request a průměrné časy byly:
Malé soubory: Time per request: 2181.758 [ms] (mean, across all concurrent requests)
Velké soubory: Time per request: 13105.619 [ms] (mean, across all concurrent requests)

Tedy 6× déle :O Ale to mi přišlo zase moc přehnané, jelikož nemám na projektu víc, než 1000 includovaných souborů (podle tracy). Tak jsem si jich udělal 1000 a v různé adresářové struktuře, aby to co nejvěrněji reflektovalo realitu projektů (max 10 souborů na složku). 1000 requestů:
Malé soubory: Time per request: 593.446 [ms] (mean, across all concurrent requests)
Velké soubory: Time per request: 602.766 [ms] (mean, across all concurrent requests)

Takže ve výsledku mi kouskování přineslo 9ms zrychlení, což je asi 1.5% zrychlení, což je docela nic v rámci toho, že pokud budu mít jeden velký soubor a rozmrskám ho na více malých (ten můj velký má 30 funkcí a malý 1), tak i kdybych byl optimista a z toho 1 velkého bych udělal jen 5 malých, tak jsem si zaprasil adresářovou strukturu, dostal jsem 1.5% k dobru, ale jak vidíme na příkladech, tak nezáleží tak moc na velikosti souboru, jako na počtu, takže ve výsledku bych tím, že jsem to rozkouskoval měl aplikaci ještě pomalejší, pokud bych musel někde načítat víc, než 1 z těch rozkouskovaných.

Což mě přivádí k tomu, že Nette stejně mapuje RobotLoaderem soubory a pokud jsou registrovány v DIC, tak při vytvoření Containeru se používají, což udělá to, že composer svým autodohledávačem je stejně naincluduje.
Takže pokud nemáš pro FrontModule a pro BackModule a tedy pro jednotlivé modelové třídy zvlášť různé DIC, tak se ty soubory stejně includnou nehledě jestli je v daném requestu použiješ, nebo ne.
Tedy tím, že to kouskuješ za účelem zrychlení vlastně aplikaci zpomaluješ.

Bulldog
Člen | 110
+
+3
-

Dokonce ji lze obejít i přes NEEXISTUJÍCÍ akci, takže autorizovat akce nedává smysl.

Nesouhlasím. Sice jde volat signály přes neexistující actiony, ale mohou být akce, které provádí efekty. Například počítají, kolikrát proběhla návštěva stránky. A neautorizované požadavky do toho počítat nechceš, takže ověřovat action je ok. Nebo dělat zbytečné dotazy do databáze a tahat si data do konkrétní šablony dané akce, pokud uživatel nemá právo. Stejně tak existenci entit chceš řešit už v akci, aby se nezačala renderovat stránka, nebo provádět handle metody atp.

Proto pro mne základním architektonickým hlediskem je pokud to jde mít v presenteru všechny akce se stejnou úrovní autorizace (role).

Jop to je dobrý. Přesně to je princip jednoakčních presenterů.

Nicméně platí, že best best practice je jednoznačně autorizace v modelu.

Obrovský a absolutní nesouhlas. Autorizaci podle mě musí řešit co nejvyšší vrstva a tedy Presenter. (Router je sice výše, ale SingleResponsibility říká, že router má routovat ne autorizovat.) Tady je pár důvodů proč:

  1. Cyklická závislost UserRepository By najednou byl závislý na Nette\Security\User, ale Authenticator aby ověřil, jestli je uživatel v databázi potřebuje právě UserRepository a vznikají cyklické závislosti.
  2. SingleResponsibility. Pokud Modelem myslíš klasicky vrstvu starající se o data v databázi, tedy repositáře, to jsou jen sluhové, kteří vykonávají činnost. Analogie: Proč by Auto mělo kontrolovat, jestli ten, kdo jej chce řídit má řidičák? O to se má starat vlastník auta a ne auto, tedy některá nadřazená vrstva.
  3. Spousta kódu navíc. Místo ověření na jednom místě v akci, jestli uživatel může vidět statistiky, budeš najednou ověřovat 10 různých modelových funkcí, které Presenter v dané akci volá.
  4. Srozumitelnost pro uživatele. Pokud máme formulář na přidání a neověřujeme v akci jestli to může vidět uživatel, tak kdokoliv může vidět stránku s formulářem, jen jim nepůjde odeslat.
  5. Lazyness. Pokud budeš ověřovat až v modelu, jak ověříš například, jestli má uživatel právo psát články? Protože tam se typicky do modelu dělají dotazy až v momentě, kdy se odešle a kompletně zpracuje formulář. Pokud to teda ověřuješ až v modelu, tak je to performance zabiják a nepomůžou ti ani kouskování souborů na menší. Ty by jsi neměl dovolit ten formulář vůbec vytvořit, pokud na to uživatel nemá právo.

Co se vykoná pokud ověřujeme až v modelu a uživatel nemá právo (a máme formulář v komponentě):

  • Zavolá se router.
  • Zavolá se konkrétní presenter
  • Nad presenterem se zavolá vytvoření komponenty s formulářem
  • nad komponentou se zavolá vytvoření formuláře
  • vytvoří se instance modelové vrstvy, která se předá formuláři a do které se natáhne user
  • na formulářové prvky se namapují POST data
  • Všechna POST data namapovaná na formulář se zvalidují pomocí výchozích validátorů
  • Formulář se zvaliduje pomocí uživatelských onValidate callbacků
  • Pokud se použil ajax, zavolá se překreslování šablon
  • z formuláře se vytáhnou hotová zvalidovaná data
  • success callback zavolá add nad modelem
  • model vyhodí exception, že uživatel nemá právo přidávat prvky
  • tohle probublá až někam do presenteru, kde to musíme odchytit a vyhodit 401err

Co se zavolá, pokud ověřujeme v akci:

  • Zavolá se router.
  • Zavolá se konkrétní presenter, který už v sobě usera má (proč asi)
  • Akce ověří, jestli má uživatel právo a ihned vyhodí 401err

Jak vidíš, tak se nemusela tvořit komponenta, nemusel se tvořit formulář, nemusela se tvořit instance modelu, nemusela se kontrolovat post data atd. Spousty věcí sis ulehčil a to nemluvě o vyhazování a odchytávání výjimek, které nebudeš potřebovat a tedy snížíš množství kódu.

  1. Nepřehlednost a duplicita ověření Ne každá akce pracuje s databází. Jsou akce, kdy chceš třeba jen poslat mail (kontaktní formulář např.) To budeš ověřovat, jestli má uživatel právo odeslat daný E-mail i v kódu, kde řešíš maily? Tedy místo, aby jsi věděl, že práva si nastavuješ na jednom místě (V Presenteru), tak budeš při změně práv/hierarchie uživatelů hledat po celém kódu, kde jsi to ověřil a kde ne? Navíc pokud tedy z nutnosti budeš ověřovat, jestli má uživatel právo na věc A, ale v jednom presenteru budeš dělat Aa, v druhém Ab a ve třetím budeš dělat Aa i Ab, ale stačí ti ověřit, jestli uživatel má právo přistupovat k A, tak musíš ověřit, jestli má právo k A jak v Aa, tak v Ab. Takže se bude v rámci 3 presenteru 2× ověřovat stejné právo.

atd. Vypisovat všechny nevýhody jsem tu do rána :D

Model je ten jediný skutečný strážce dat, best best je, aby autorizoval on.

Model je strážce dat? Ok možná souhlas. Je best aby autorizoval on? Nope.
viz příklad s autem.

A NIKDY nedávám do jednoho presenteru akce, kde by se úroveň autorizace lišila. Moje presentery jsou malé, obvykle tříakční – list, create, update

A přesně tady se úrovně autorizace běžně liší.

  • List – většinou může každý
  • Create – Většinou může jen přihlášený uživatel
  • Update – Většinou může jen vlastník dané resource

A při analýze jednoduše v návrhu rolí se snažím do tohoto schématu trefit.

To mi přijde jako chyba. Podle mě by aplikace měla reflektovat přání zákazníka. Ne přání zákazníka ohýbat podle aplikace.

Bulldog
Člen | 110
+
0
-

Asi to bude hodně individuální, ale já preferuji vedle minimalizace kódu ještě čitelnost. A mě osobně vyhovuje princip střední cesty – co nejstručnější kód, ale ne do extrému.

Tady zase souhlasím spíše s Kamilem.
Vem si například, že máš ten tvůj presenter se 4 akcemi (navíc detail) a jedná se opět o články. Chceš mít přidání článku, editaci, seznam článků, detail článku a v detailu ještě navíc seznam komentářů a přidání komentáře.
Presenter bez trait:

class Presenter extends Ui\Presenter
{
	#[Inject]
	public ArticleAddFormFactory $articleAddFormFactory;
	#[Inject]
	public ArticleUpdateFormFactory $articleUpdateFormFactory;
	#[Inject]
	public ArticleListFactory $articleListFactory;
	#[Inject]
	public ArticleDetailFactory $articleDetailFactory;
	#[Inject]
	public ArticleCommentListFactory $articleCommentListFactory;
	#[Inject]
	public ArticleCommentAddFormFactory $articleCommentAddFormFactory;
	#[Inject]
	public ArticleRepository $articleRepository;

	private Article $article;

	public function actionUpdate(int $id): void
	{
		$this->article = $this->articleRepository->get($id);
	}

	public function createComponentArticleAddFormFactory(): Form
	{
		return $this->articleAddFormFactory->create();
	}

	public funcion createComponentArticleUpdateFormFactory(): Form
	{
		return $this->articleUpdateFormFactory->create();
	}

	public function createComponentArticleListFactory(): Control
	{
		return $this->articleListFactory->create();
	}

	public function ArticleDetailFactory(): Control
	{
		return $this->articleDetailFactory->create();
	}

	public function ArticleCommentListFactory(): Control
	{
		return $this->articleCommentListFactory->create();
	}

	public function createComponentArticleCommentAddFormFactory(): Form
	{
		return $this->articleCommentAddFormFactory->create();
	}
}

A s traitami:

class Presenter extends Ui\Presenter
{
	use ArticleAddFormTrait;
	use ArticleUpdateFormTrait;
	use ArticleListTrait;
	use ArticleDetailTrait;
	use ArticleCommentListTrait;
	use ArticleCommentAddFormTrait;

	#[Inject]
	public ArticleRepository $articleRepository;

	// Tuhle proměnnou obsahuje traita ArticleUpdateFormTrait, která ji využívá. mít ji ale pro přehlednost i tady neuškodí
	private Article $article;

	public function actionUpdate(int $id): void
	{
		$this->article = $this->articleRepository->get($id);
	}
}

Nemůžeš popřít, že to s traitami je 100% přehlednější. Zvlášť, když víš, na co ty traity jsou a tedy, že jejich obsah je vždy podobný, takže si jde jednoduše představit, co se za tím skrývá…

Samozřejmě další hromady výhod jsme již nastínili (testování, znovupoužitelnost…)

v podstatě se mě nestává, že bych aplikaci navrhnul tak, aby vznikla potřeba mít formulář ve dvou/třech presenterech

Opět to je podle mě na přání zákazníka. Pokud chce zákazník výpis článků na frontendu, na backendu a ještě nějakou light verzi výpisu na homepage, tak zákazníkovi neřeknu hele já mám aplikaci navrženou takto, tak bohužel budeš to mít jen na 1 místě a smiř se s tím. Nope. Prostě dodám do továrny nastavení, kdy homepage presenter si nastaví, že chce 4 články bez paginátoru a klasický seznam jich bude mít 20 s paginátorem

To vše se bude nastavovat v traitě a tu budu mít na všech místech ve všech presenterech.

Takže když dobře analyzuješ a navrhuješ aplikaci, tak máš velkou většinu formulářů jen pro jeden presenter.

To tak často bývá i při špatném návrhu. Otázka je, jestli testuješ a jestli ti nevadí zaneřáděný presenter opakujícím se kódem atp. No a hlavně jestli ti nevadí, že budeš v případě, že vyjde najevo nový presenter, který to potřebuje, tak že budeš kód kopírovat, upravovat, předělávat do traity atp… nebo jestli ti nevadí, že když se změní nastavení komponenty/formu, tak že to budeš pak upravovat na více místech.
Nebo jestli ti nevadí, že když budeš na jiném webu dělat tu stejnou věc, tak ti nebude stačit CTRL+C v jednom projektu a CTRL+V v druhém a napsání use ArticleAddFormTrait; všude kde to potřebuješ, ale budeš muset překopírovávat i to nastavení atp.

nadbytečné bolierplate soubory

Co mě irituje, ale vím že to tak technicky musí být jsou nadbytečné soubory jako třeba soubory pro automatické generování factory pro formuláře/komponenty. Mám tak ke každému souboru s formulářem navíc jeden servisní s interface jenom proto, že to framework zatím neumí udělat jinak bez těch souborů.

Tak tady jde o návrhový vzor Factory. Ten ‚boilerplate‘ soubor má hromady svých účelů a oplatí se jej mít. Minimálně na testování. Taky když budeš předávat závislosti do komponenty/formu atp., tak se o to postará Nette a nemusíš to psát ručně. Je to interface, takže za něj můžeš dosadit libovolný mock. A v neposlední řadě. Díky němu můžeš tvořit hromady unikátních komponent, které se ti nebudou navzájem přepisovat a nebudeš mít natvrdo spojené závislosti pomocí slovíčka new.

Nevýhoda samozřejmě je, že musí existovat, ale to není problém Nette, ale jazyka PHP. Kdyby třídy byly zároveň interfaces, tak by se daly továrny generovat přímo ze samotného souboru například.
pokud ti to vadí udělej si nějakou globální továrnu, keteré předáš třídu co chceš vytvořit a ona ji vytvoří :D Ale to nedoporučuju.

No a tvorba této továrny je easy. Napíšeš ji jednou a pak jen kopíruješ (Chytré IDE ti i změní namespace) takže se nestaráš. Jelikož všechny komponenty nazývám jednotně Control, tak moje továrna vypadá v 90% případů takto:

<?php declare(strict_types=1);

namespace App\Components\Article\Grid;

interface ControlFactory
{
    public function create(): Control;	// Kde Control je má komponenta ve stejné složce
}

Takže jen copypasta a žádný pain to nedělá.

Ohledně boilerplate souboru s traitou, tak to nesouhlasím.
To, co je v traitě by bylo jinak v presenteru. Takže overload slovíček je minimální.

presenter bude čistší, ale za jakou cenu ??

Právě za velmi malou cenu. Jelikož jako benefit nemáš jen čistotu presenteru, ale sousty dalších výhod již popsaných

Takže takhle používat traitu na vkládání formulářů nedává smysl.

Právě že dává viz výše.

obyčejný $appDir a tak složité to je

Bohužel je to složitý, nečitelný kód,

Hej no. Jako je. :D Ale to je jen jeden způsob z několika. Můžeš podědit template factory a inicializovat to tam. Můžeš to řešit v Presenterech :D Můžeš si vytvořit vlastní provider, který předáš enginu. Je tam toho spousty.
já osobně jsem $appDir v šablonách nikdy nepotřeboval. Znovupoužitelné prvky dělám jako komponenty a ty mi tedy načítá presenter, takže nějaké includování šablon pff..

Hlavně ale jde o to, že tenhle kód nemusí být čitelný ani přehledný.

  1. má pár řádků, takže who cares
  2. pouze nastavuje proměnné, takže je to takový statický dataprovider, který nemusí být přehledný, jelikož v něm není žádný sofistikovaný kód, který by se mohl pokazit a musel by jsi ho debugovat. Je to prostě jen statické přidání proměnných
  3. Napíšeš to jednou a pak už na to nesáhneš. A když náhodou sáhneš, tak hej přidáš 1 řádek do třídy. Thats all
  4. Když to potřebuješ v jiném projektu? Copypasta. 1× zkopíruješ a máš zase na měsíc klid.
m.brecher
Generous Backer | 758
+
-3
-

@Bulldog

Pokud Modelem myslíš klasicky vrstvu starající se o data v databázi, tedy repositáře, to jsou jen sluhové, kteří vykonávají činnost.

Částečný souhlas. Většina modelových tříd mají být vykonavatelé – ano. Ale ne všechny modelové třídy. Ve složitějších aplikacích může provedení nějaké akce v databázi – např. pokyn k zahájení realizace složitější zakázky záviset na více faktorech než jenom role přihlášeného uživatele. Může to být i poměrně složité – uživatel nemá oprávnění, ale kolega, který oprávnění má je na dovolené a v té době ho zastupuje a přechodně oprávnění má, ale pro omezené akce např. zakázky do 300 000 Kč. Zahájení zakázky může záviset na předchozím schválení šéfe, na situaci na skladě dílů na potvrzení a termínech nějakých subdodávek.

Když jsem pro takovéto a podobné požadavky navrhoval Model, tak jsem na to šel specializovanou modelovou třídou pro ověření – zda lze akci provést nebo ne. A nejednalo se vždycky jenom o přidělené role k určitému resource. Modelová třída pro provedení akce neměla ponětí zda se to může provést ale zeptala se třídy, která to věděla – každá třída měla jeden jediný účel. No a buďto to provedla, nebo ne.

Je samozřejmě možný i scénář, že se presenter zeptá nejdřív ověřovací třídy a podle výsledku volá akční třídu, ale lepší je nechat to přímo na modelu, ať on za to zodpovídá. I v tom případě, kdy formálně řídí presenter, tak ve skutečnosti řídí podle dat z databáze, které mu dodal model. Dokonce i v triviálním případě, kdy se presenter zeptá vestavěné třídy pro správu identit, tak role v identitě se získala při přihlášení z nějaké modelové třídy která ji vysosla z databáze. No a skutečnost, že vývojář zadrátuje v kódu presenteru konkrétní roli přihlášené identity ke konkrétní akci modelu, který je jenom vykonavatelem, je ve své analytické podstatě „datové“ propojení role s akcí presenteru, které by se dalo realizovat rovnou v modelu a presenter vynechat. Ale v jednoduchých aplikacích je to takhle OK.

Veškerá odborná literatura ohledně Modelu co znám se shoduje na tom, že Model reprezentuje bussiness logiku aplikace, kam patří i nejrůznější role, oprávnění a pravidla.

Ne ne, mě nikdo nemůže přesvědčit, že Model je pouhý vykonavatel. S tebou se skvěle diskutuje, ono by se lépe diskutovalo nad sklenkou oroseného. Další témata která exponenciálně v této diskuzi narůstají by se tam skvěle probraly.

Editoval m.brecher (20. 10. 2022 3:06)

m.brecher
Generous Backer | 758
+
0
-

@Bulldog

Tohle vypadá fakt hezky a přehledně, takže se nad traitami a jejich potenciálem budu muset vážně zamyslet:

class Presenter extends Ui\Presenter
{
	use ArticleAddFormTrait;
	use ArticleUpdateFormTrait;
	use ArticleListTrait;
	use ArticleDetailTrait;
	use ArticleCommentListTrait;
	use ArticleCommentAddFormTrait;

	#[Inject]
	public ArticleRepository $articleRepository;

	// Tuhle proměnnou obsahuje traita ArticleUpdateFormTrait, která ji využívá. mít ji ale pro přehlednost i tady neuškodí
	private Article $article;

	public function actionUpdate(int $id): void
	{
		$this->article = $this->articleRepository->get($id);
	}
}
Bulldog
Člen | 110
+
0
-

@mbrecher

Ne ne, mě nikdo nemůže přesvědčit, že Model je pouhý vykonavatel.

ono je hlavně potřeba si předem ujasnit, o čem mluvíme :D Samozřejmě jak píšeš, tak Model z MVC je opravdu kompletně celá Buisness logika. Tedy technicky tam patří i router, authorizator, authenticator atp.
Bohužel největší část Buisness logiky je práce s daty (což je i smysl aplikace) a když se podíváš jak se to interpretuje v tutoriálech apod., tak právě říkají, že model je na práci s daty.

Proto hodně programátorů pochopí jako model pouze entity a repozitáře. Když se pak podíváš do adresářové struktury různých cizích projektů, tak uvidíš, že existuje například složka Router, která má v sobě Router, který je model, pak je třeba složka Helpers, kde jsou srandy co používají na více projektech, složka Security, kde je authorizator a authenticator atp. No a nakonec je tam složka Model, která obsahuje pouze entity a repozitáře.

Dokonce kolega byl jednou nařčen, že nemá žádný model, když používá NetteDatabase a tedy nemá nic jako repozitáře a Entity.

Takže interpretovat slovo model můžeš 2 způsoby.

  1. Třídy starající se o reprezentaci a manipulaci s daty. (Entity a repozitáře)
  2. Komplet celá buisness logika.

Je to podobný případ, jako slovo Backend. Dokud neznáš kontext, nebo si to neujasníš, tak nevíš, jestli se bavíš o kódu běžícím na serveru, nebo o části aplikace, která je přístupná pouze adminům :D

Ale v obou případech by neměla v Modelu probíhat autorizace.

Model obecně má definovat Autentizační i Autorizační pravidla i logiku.
Ale samotnou autorizaci by provádět neměl.

V tvém příkladu: (navrhnu jen jedno z mnoha řešení)

Může to být i poměrně složité – uživatel nemá oprávnění, ale kolega, který oprávnění má je na dovolené a v té době ho zastupuje a přechodně oprávnění má, ale pro omezené akce např. zakázky do 300 000 Kč. Zahájení zakázky může záviset na předchozím schválení šéfe, na situaci na skladě dílů na potvrzení a termínech nějakých subdodávek.

Pracujeme tedy s následujícími věcmi:

  • Order – Entita implementující Nette\Security\Resource
  • OrderRepository – Chápeme, takže nebude v příkladech (má jen getById metodu)
  • MerchantRepository – Chápeme, takže nebude v příkladech (má jen getById metodu)
  • Merchant – Někdo, kdo manipuluje se zakázkami jemu přidělenými a může zastupovat jiného obchodníka.
  • OrderUpdatePresenter – Chápeme
  • Authenticator – Přihlášení uživatele, definuje role
  • AuthorizatorFactory – Definuje ACL pro uživatele
  • MerchantRole – Role definující obchodníka
  • RepresentativeRole – Role definující zástupce kolegy

Teď nastíním, jak budou jednotlivé třídy vypadat (kromě těch, které jsou jasné. – Použil jsem v příkladu NextrasORM)
Order:

/**
 * @property int $id {primary}
 *
 * @property int $price
 *
 * @property bool $isApproved
 * @property bool $hasEnoughMaterial
 *
 * @property Merchant $merchant {m:1 Merchant::$orders}
 */
class Order extends \Nextras\Orm\Entity\Entity implements Nette\Security\Resource
{
    public function getResourceId(): string
    {
        return 'Order';
    }
}

Merchant: (zjednodušený)

/**
 * @property int $id {primary}
 *
 * @property string $password
 *
 * @property Merchant|null $representativeOf                        {1:1 Merchant, isMain=true, oneSided=true}
 * @property Nextras\Orm\Relationships\OneHasMany<Order> $orders    {1:m Order::$merchant}
 */
class Merchant extends \Nextras\Orm\Entity\Entity { }

Authenticator: (pouze přiřazuje uživateli role na základě dat.)

class Authenticator implements Nette\Security\Authenticator
{
    public function __construct(
        private readonly MerchantRepository $merchantRepository,
        private readonly Nette\Security\Passwords $passwords,
    )
    {
    }

    public function authenticate(string $user, string $password): Nette\Security\IIdentity
    {
        $merchant = $this->merchantRepository->getById($user);

        if (!$merchant ||
            !$this->passwords->verify($password, $merchant->password)) {
            throw new Nette\Security\AuthenticationException('Error :))', 1);
        }

        $roles = [
            new MerchantRole($merchant->id),
        ];

        if ($merchant->representativeOf) {
            $roles[] = new RepresentativeRole($merchant->representativeOf->id);
        }

        return new Nette\Security\SimpleIdentity(
            $merchant->id,
            $roles,
            [], // Nějaká data?
        );
    }
}

MerchantRole:

class MerchantRole implements Nette\Security\Role
{
    public function __construct(
        private readonly int $id,
    ) { }

    public function getRoleId(): string
    {
        return 'Merchant';
    }

    public function getMerchantId(): int
    {
        return $this->id;
    }
}

RepresentativeRole: (nešťastně nazváno, ale jde o roli, která říká, že někoho zatupuju a funkce getRepresentativeId vrací ID koho)

class RepresentativeRole implements Nette\Security\Role
{
    public function __construct(
        private readonly int $id,
    ) { }

    public function getRoleId(): string
    {
        return 'Representative';
    }

    public function getRepresentativeId(): int
    {
        return $this->id;
    }
}

AuthorizatorFactory: (pouze definuje bezpečnostní omezení)

class AuthorizatorFactory
{
    public static function create(): Nette\Security\Permission
    {
        $acl = new Nette\Security\Permission();

        $acl->addResource('Order');

        $acl->addRole('Merchant');
        $acl->addRole('Representative');

        // Pro jistotu vše zakážu
        $acl->deny('Merchant');
        $acl->deny('Representative');

        $acl->allow('Merchant', 'Order', 'update', self::canMerchantUpdateOrder(...));
        $acl->allow('Representative', 'Order', 'update', self::canMerchantUpdateOrder(...));

        return $acl;
    }

    private static function canMerchantUpdateOrder(Nette\Security\Permission $acl, string $role, string $resource, string $privilege): bool
    {
        $role = $acl->getQueriedRole();
        assert($role instanceof MerchantRole);
        $resource = $acl->getQueriedResource();
        assert($resource instanceof Order);

        // Může editovat, pokud to schválil šéfík a máme dost matroše a je její vlastní obchodník
        return $resource->isApproved &&
               $resource->hasEnoughMaterial &&
               $role->getMerchantId() === $resource->merchant->id;
    }

    private static function canRepresentativeUpdateOrder(Nette\Security\Permission $acl, string $role, string $resource, string $privilege): bool
    {
        $role = $acl->getQueriedRole();
        assert($role instanceof RepresentativeRole);
        $resource = $acl->getQueriedResource();
        assert($resource instanceof Order);

        //  Může editovat, pokud to schválil šéfík a máme dost matroše a pokud zastupuje vlastního obchodníka objednávky a zároveň není její cena větší, než 300 tisíc
        return $resource->isApproved &&
               $resource->hasEnoughMaterial &&
               $resource->price < 300_000 &&
               $role->getRepresentativeId() === $resource->merchant->id;
    }
}

No a nakonec Presenter:

class OrderUpdatePresenter extends Nette\Application\UI\Presenter
{
    #[Nette\DI\Attributes\Inject]
    public OrderRepository $orderRepository;

    protected function startup()
    {
        parent::startup();

        $id = $this->getHttpRequest()->getQuery('id');
        $order = $this->orderRepository->getById($id);

        if (!$order) {
            $this->error('Not found', 404);
        }

        // No a nakonec samostatné ověření práv
        if (!$this->user->isAllowed($order, 'update')) {
            $this->error('Not Allowed', 401);
        }
    }
}

Thats All. Model pouze pracuje s daty. Ano, i definice přístupových práv je práce s daty. Jak vidíš i složitější logika jak píšeš se dá jednoduše napsat tak, aby práva ověřoval Presenter.
Tohle je vlastně základ všech certifikačních autorit.

Editoval Bulldog (20. 10. 2022 4:51)

Bulldog
Člen | 110
+
0
-

Samozřejmě můžeš mít ten if s isAllowed i v metodě OrderRepository::getById. Ale tam budeš řešit hromadu srand jako například:

  • Chce to získat protože se chce podívat, což může, nebo to chce získat, aby to editoval, což nemůže?
  • Kde vezmu instanci třídy Nette\Security\User, který má v sobě ACL s definovanými přístupovými pravidly, abych to ověřil?
  • Jaké mám vyhazovat výjimky? Když nenajdu objednávku, mám vyhodit výjimku že neexistuje, nebo je to v pohodě, protože někdy počítáme s neexistencí a očekáváme NULL jako návratovou hodnotu? Jelikož třída nemá být závislá na tom, kdo ji volá tak jak rozhodneš, jak se to má zachovat?
  • Pokud máme při neexistenci vracet NULL budeš mít v metodě getById mít další if který řekne pokud objednávka neexistuje vrať ji, ale pokud existuje, tak ověř jestli máme právo a v presenteru pak budeš znovu ověřovat ifem jestli není null aby jsi vyhodil 404?
  • Budeš v presenteru catchovat exceptionu vyhozenou modelem aby jsi hned na to vyhodil výjimku ukončující kód s 401?

atd.

Marek Bartoš
Nette Blogger | 1171
+
+3
-

Pokud repozitáře kontrolují oprávnění, tak porušují single responsibility principle.
Data mohou získávat i crony a konzolové tasky, ve kterých se žádná oprávnění nekontrolují.
Do API se přihlašuji s jiným storage a jinou metodou ověřování, nestačí tak pouze jeden user.
Dokonce máme v administraci a na veřejné části webu současně přihlášené jiné uživatele a komponenty z administrace se zobrazují na veřejné části. Musel bych tak v každé komponentě do modelu předávat uživatele.

m.brecher
Generous Backer | 758
+
0
-

Marek Bartoš napsal(a):

Pokud repozitáře kontrolují oprávnění, tak porušují single responsibility principle.

Samozřejmě, tak jsem to myslel – jedna modelová třída vykonává akce bez námitek a jiná modelová třída ověřuje, zda lze akci vykonat – různá oprávnění a jiné podmínky. Tohle spíš @Bulldog pochopil jinak než jsem myslel.

Tam byl ale jiný principiální rozpor v naší diskusi. A sice můj nesouhlas s tím, že autorizuje vždycky jenom presenter protože je nadřízený a model vykonává. Já jsem psal, že jsou možné dva přístupy – a) presenter získá autorizační povolenku k nějaké akci z Authorizatoru (součást modelu, třeba nějaké ACLko) a pak volá/nevolá model k akci, b) presenter předá pokyn k akci modelu a akční modelová třída sama požádá o autorizaci Authorizator. IMHO je možné obojí a záleží na situaci co je výhodnější.

Jak píšeš, že různé konzolové skripty mohou model obejít, samozřejmě a nejen cli, můžeme mít v aplikaci zastrčený presenter, kde se na autorizaci zapomene a kterým se obejde ten hlavní, kde je autorizace funkční. Proto taká já říkám, že nejspolehlivější je validace a autorizace v modelu.

Konzolové skripty jsou uživatel jako každý jiný a měly by mít v authorizátoru vymezeny nějaké mantinely co mohou/nemohou dělat, aby mohly vykonat co mají a nenapáchaly škody. Záleží co se s nimi provádí.

Proto jsem názoru, že čím blíže jsou důležité datové kontroly k vlastním datům, tím lépe. Kdyby konzolové skripty využívaly model aplikace a model by měl neprůstřelnou validaci a autorizaci, tak jsou data zabezpečena i před cli skripty.

Pojďme si to shrnout ještě jednou to nejdůležitější:

Přístup a) Presenter získá výsledek autorizace z Authorizátoru a pak volá/nevolá akční model k provedení akce, ten už nevaliduje a pouze vykoná, zde existuje riziko obejití presenteru a zavolání modelu, který autorizovanou akci provede bez autorizace

Přístup b) Presenter provede základní ověření oprávnění pro celý presenter a pak předá požadavek modelu k provedení akce, akční modelová třída volá před akcí autorizátor a podle výsledku akci provede/vyhodí výjimku, zde to hacker bude mít obtížnější

Presenter mohu obejít i já jako programátor nějakým omylem v jiném presenteru. CRON jak ho mám já na webhostingu volá skript php přes https, tam se volá CronPresenter s příslušnou autorizací pro konkrétní akci, ano zde je potřeba maximální opatrnost.

m.brecher
Generous Backer | 758
+
0
-

@Bulldog

Dokonce ji lze obejít i přes NEEXISTUJÍCÍ akci, takže autorizovat akce nedává smysl.

Tady jsem měl na mysli KLÍČOVÉ autorizace, na fóru proběhla před časem hezká diskuze, kde se člen komunity Nette rozčiloval, že jeho aplikaci postavenou v Nette presenterech bylo možné snadno hacknout tak, že se odeslal podvržený požadavek registračního formuláře. On autorizoval v render<View> :). Když jsem si ti doma testoval, přišel jsem na to, že podvržený formulář se zpracuje i v neexistující akci a chyba 404 (šablona akce neexistuje) se vyhodí po hacknutí databáze. Vizuálně není nic vidět, ale průšvih je. I kdyby ten vývojář zautorizoval VŠECHNY action<Action> v presenteru, nepomůže to, vždycky to jde obejít neexistující akcí, kterou autorizovat nejde. Proto důležité autorizace pouze v startup(). Nekritické, kde nic moc nehrozí (hacknutí počítadla je ok) klidně ano.

m.brecher
Generous Backer | 758
+
0
-

@Bulldog

Což mě přivádí k tomu, že Nette stejně mapuje RobotLoaderem soubory a pokud jsou registrovány v DIC, tak při vytvoření Containeru se používají, což udělá to, že composer svým autodohledávačem je stejně naincluduje.
Takže pokud nemáš pro FrontModule a pro BackModule a tedy pro jednotlivé modelové třídy zvlášť různé DIC, tak se ty soubory stejně includnou nehledě jestli je v daném requestu použiješ, nebo ne.
Tedy tím, že to kouskuješ za účelem zrychlení vlastně aplikaci zpomaluješ.

Díky moc za důkladnou analýzu vlivu velikosti include souboru na rychlost aplikace.

Co mě překvapuje, ale zase ne až tak moc, že se i soubory nepoužívaných tříd includnou. Mě ukazuje Tracy cca 170 includovaných souborů i pro obyčejné Hello World. Je to doma s SSD diskem cca 100 ms, ale nezlobil bych se, kdyby to bylo svižnější cca alespoň do 50 ms. Dotazy do databáze na čas nemají absolutně vliv pohybují se cca 6 dotazů do 2 ms. Takže to bude nejspíš v includování, protože vykonaného kódu zase tolik nebude. Tohle je také zajímavé téma na diskuzi. Dříve, když jsem ještě matlal php se stránka vyrendrovala daleko rychleji. Něco budu mít nedobře výkonnostně nastavené. Na hostingu to nejede o moc rychleji.

Editoval m.brecher (20. 10. 2022 19:53)

m.brecher
Generous Backer | 758
+
+1
-

@Bulldog

Tedy tím, že to kouskuješ za účelem zrychlení vlastně aplikaci zpomaluješ.

Vývoj toho modelu o němž mluvíme se nějak vyvíjel a nakonec skončil tak, že Front module používal svoje metody a Admin modul používal ty ostatní a nepřekrývalo se to. Intuitivně dle hlasu rozumu jsem to rozdělil – sice výkonnostně je to jedno, ale je to v souladu s intuicí. Když jsem si opakoval SOLID základy, tak mě napadlo že známé „Clients should not be forced to depend upon interfaces that they do not use“ je možná i tento případ. Jestliže lze modely rozdělit na dvě části a klienti (moduly) s tím nemají problém a ukáže se, že ve skutečnosti každý závisí na tom svém modelu, tak jsem vlastně v původní verzi spojoval v jednom interface dohromady dva interface, které byly z podstaty nezávislé. Ta nezávislost je dána koncepcí Front podle slugu, Admin podle id.

Kamil Valenta
Člen | 762
+
+1
-

m.brecher napsal(a):
Proto důležité autorizace pouze v startup().

Zbytečně se autorizátor připraví o dílčí resources. Samozřejmě, že tak jak se autorizují actiony a rendery, lze autorizovat i handly. A to ničím neexistujícím neobejdeš…

Kamil Valenta
Člen | 762
+
+1
-

m.brecher napsal(a):
… ale je to v souladu s intuicí. Ta nezávislost je dána koncepcí Front podle slugu, Admin podle id.

Mně zase intuice říká, že „za půl roku“ přibude požadavek na archivaci entit a Ty to budeš muset upravovat na 2, nebo dokonce více místech. Když je to normálně v jednom modelu, tak si jen opravíš metodu, která vrací základní selectionu a je Ti už celkem jedno, že nad ní nějaká metoda volá where(‚id‘, 5) a jiná where(‚slug‘, ‚ahoj‘)…

m.brecher
Generous Backer | 758
+
0
-

Kamil Valenta napsal(a):

m.brecher napsal(a):
… ale je to v souladu s intuicí. Ta nezávislost je dána koncepcí Front podle slugu, Admin podle id.

Mně zase intuice říká, že „za půl roku“ přibude požadavek na archivaci entit a Ty to budeš muset upravovat na 2, nebo dokonce více místech. Když je to normálně v jednom modelu, tak si jen opravíš metodu, která vrací základní selectionu a je Ti už celkem jedno, že nad ní nějaká metoda volá where(‚id‘, 5) a jiná where(‚slug‘, ‚ahoj‘)…

Pod pojmem archivace entit nevím co si mám představit – archivaci dat v databázi? To se těžko bude týkat readonly modelu pro Front, nebo jsem to špatně pochopil ?

m.brecher
Generous Backer | 758
+
-2
-

Kamil Valenta napsal(a):

m.brecher napsal(a):
Proto důležité autorizace pouze v startup().

Zbytečně se autorizátor připraví o dílčí resources. Samozřejmě, že tak jak se autorizují actiony a rendery, lze autorizovat i handly. A to ničím neexistujícím neobejdeš…

To je pravda, můžeme autorizovat komponenty, handly, cokoliv a může to fungovat dobře, ale jak už jsem před rokem diskutoval s @uživatel-p když se autorizace rozdrobí na různá místa, bude méně přehledná a riziko chyby větší. Proto VŽDY a JEDINĚ autorizace ve startup a výjimky jsou možné, ale jenom když je pro to silný důvod.

Editoval m.brecher (20. 10. 2022 21:23)

m.brecher
Generous Backer | 758
+
0
-

@KamilValenta

Zbytečně se autorizátor připraví o dílčí resources. Samozřejmě, že tak jak se autorizují actiony a rendery, lze autorizovat i handly. A to ničím neexistujícím neobejdeš…

Bohužel s ACL jsem ještě neimplemenoval, takže nechci polemizovat s něčím, co moc neznám.

m.brecher
Generous Backer | 758
+
-3
-

@Bulldog a @KamilValenta

Ohledně autorizací bych to shrnul, protože se to rozplizlo do řady atomických myšlenek. IMHO je dobré se držet v presenterech Nette jednoduchých zásad:

čím méně je míst, kde se autorizuje tím lépe kvůli přehlednosti

nemíchat v presenterech výrazně odlišné úrovně autorizace, typicky oddělit přihlášené/nepřihlášené a z přihlášených admina od běžných uživatelů

základní autorizaci provádět vždy ve startup pro celý presenter

dát si dobrý pozor když se autorizuje v akci – samozřejmě to jde použít také

právě s ohledem na jednoduchost a důležitost autorizace přihlášených uživatelů je ideální řešení AdminPresenter který na jednom místě dělá důležitou základní autorizaci pro celou administraci

jakmile máme klíčovou autorizaci v presenteru za sebou, může následovat jemnější – podle resource, podle autorství záznamu v tabulce, podle dovolené kolegy v oddělení, atd…

autorizaci může provádět nejen presenter ve spolupráci a autorizátorem, ale i model sám ve spolupráci s autorizátorem, záleží na projektu a na charateru bussiness logiky

Na tom bychom se měli všichni shodnout, diskutovat o detailech by bylo možné, ale tam záleží na konkrétním projektu.

Editoval m.brecher (20. 10. 2022 21:40)

Marek Znojil
Člen | 77
+
+2
-

m.brecher napsal(a):
základní autorizaci provádět vždy ve startup pro celý presenter

Ještě lepší je použít metodu k tomu určenou:
https://github.com/…omponent.php#L125

Bulldog
Člen | 110
+
0
-

@MarekZnojil
Good point.

@mbrecher
jak píše @KamilValenta autorizovat se dají i handly a komponenty. A tuhle autorizaci prostě ničím neobejdeš. Při requestu se spustí Presenter, což je entrypoint aplikace a nemáš jak jinak model zavolat než právě z presenteru. Takže utnout vykonávání hned v něm dává největší smysl.
Tím, že já osobně nemám v presenterech žádné handly, tak musím ověřovat jen actiony a komponenty, takže kód, aby hacker nevykonal co nechci může vypadat třeba takto:

class OrderPresenter extends Nette\Application\UI\Presenter
{
    #[Nette\DI\Attributes\Inject]
    public OrderRepository $orderRepository;
	private bool $orderEditRight = false;	// Když je nastavím na false, tak vím, že se tam nikdo nedostane, pokud to explicitně nepovolím
	private bool $orderAddRight = false;
	private Order $order;


    public function actionEdit(int $id)
    {
        $order = $this->orderRepository->getById($id);

        if (!$order) {
            $this->error('Not found', 404);
        }
        if (!$this->user->isAllowed($order, 'edit')) {
            $this->error('Not Allowed', 401);
        }

		$this->order = $order;
		$this->orderEditRight = true;
    }

    public function actionAdd()
    {
        if (!$this->user->isAllowed('order', 'add')) {
            $this->error('Not Allowed', 401);
        }
		$this->orderAddRight = true;
    }

	public function createComponentOrderEdit(): Form
	{
		if (!$this->orderEditRight) {
            $this->error('Not Allowed', 401);
        }
		... // samotná tvorba
	}

	public function createComponentOrderAdd(): Form
	{
		if (!$this->orderAddRight) {
            $this->error('Not Allowed', 401);
        }
		... // samotná tvorba
	}
}

Easy Peasy. Toto už nikdo nikdy neobejde a nezavolá danou komponentu pokud nemá práva, která ověřuje action metoda.

Navíc, pokud používáš ty traity, tak si můžeš vytvořit jednu traitu, kterou mám já osobně nazvanou jako SecurityPresenterTrait, která mi přetěžuje presenterovskou protected function createComponent(string $name): ?Nette\ComponentModel\IComponent a automaticky na základě řetězce $name kontroluje, jestli má uživatel přístup v momentě vytvoření komponenty, takže si ten if v každé CreateComponent<name> metodě můžu odpustit a vím, že to mám secure.
Teď mě tak napadá, že můžeš obsah té SecurityPresenterTrait mrsknout i do BasePresenteru :D Ale hej já to mám radši jako traitu.

To je pravda, můžeme autorizovat komponenty, handly, cokoliv a může to fungovat dobře, ale jak už jsem před rokem diskutoval s @uživatel-p když se autorizace rozdrobí na různá místa, bude méně přehledná a riziko chyby větší. Proto VŽDY a JEDINĚ autorizace ve startup a výjimky jsou možné, ale jenom když je pro to silný důvod.

Teď jsi popsal přesně důvod, proč existují jednoakční presentery

Konzolové skripty jsou uživatel jako každý jiný a měly by mít v authorizátoru vymezeny nějaké mantinely co mohou/nemohou dělat, aby mohly vykonat co mají a nenapáchaly škody. Záleží co se s nimi provádí.

Ne nutně. Záleží, jak to máš vyřešeno. Pokud spouštíš ‚CLI‘ požadavky jako klasický request na aplikaci, což nejsou CLI požadavky, jelikož CLI je přes příkazovou řádku a tedy odteď tomu budeme říkat třeba Cron požadavky, tak stačí mít presenter, který se stará jen o tyto Cron požadavky a ideálně to mít nastaveno tak, aby když ten Cron požadavek odposlechne někdo zvenku a začne jej 40* za sekundu opakovat, tak to aplikaci nerozbije.
Hezky se to řeší například ukládáním posledního exec time a ignorováním nadbytečných požadavků, nebo tím, že to nedělá žádné side efekty. Například pokud se generují feedy, tak ničemu nevadí, když je útočník vygeneruje 40× za vteřinu než 1× za hodinu. Akorát to vytíží server. Nebo když to rozesílá maily, tak mít u každého mailu nastaveno datum odeslání a tedy při opakovaném požadavku se nic nevykoná atp.
A jelikož cron požadavky nepotřebují odpovědi jako klasický uživatel, tak nemusí existovat žádné handle metody ani rendery nic. Stačí, jen když konkrétní action spustí model a odešle payload/response.
No a když se spouští jen actiony a nemáš komponenty ani handly, tak kde jinde, než v action dělat ověření?

Když to nebudou cron requesty přes HTTP, ale budou to třeba REST API požadavky, tak tam je to ověřování úplně stejné jako v případě běžného uživatele. Jediný rozdíl tam je v tom, že přes REST API místo HTML stránky vrátíš JSON, jinak je to totožná věc, což už jsme vyřešili.

A pokud jde o CLI požadavky, tak tam ověřování ani nepotřebuješ, protože víš, že se vždy spouští z konzole a tedy že tam útočník není, protože o to aby tam útočník nebyl se už stará server, který má třeba zabezpečený SSH přístup. A pokud ti útočník nabourá SSH přístup, tak ti zabezpečení CLI skriptu je na houby, jelikož má útočník přístup k souborům a prostě zabezpečení vymaže…
Taky v rámci CLI requestů máš většinou osekaný DIC, který má jen to, co potřebuješ a nenačítá všechny třídy a závislosti, jako jsou třeba presentery, router, lattečko atp., protože v konzolovce to nepotřebuješ (pokud nepředgenerováváš šablony, nebo posíláš maily. Pak Latte potřebuješ :D ) a entrypoint pro tvorbu produkt feedů pro srovnávače může vypadat třeba takto:

#!/usr/bin/env php
<?php declare(strict_types = 1);

require __DIR__ . '/../vendor/autoload.php';

exit(App\Bootstrap::bootCliProductFeed()	// bootCliProductFeed mi načte DIC, který má v sobě jen ProductRepository a ProductProcessor
	->createContainer()
	->getByType(ProductProcessor::class)
	->process());
// Takže jsem si vytáhl z DIC jen konkrétní modelovou třídu a nad ní jsem spustil to, co má dělat.

A obsah ProductProcessoru:

class ProductProcessor
{
    use SmartObject;

    public function __construct(
        private readonly ProductRepository $productRepository,
    ) {}

    public function process(): void
    {
        $data = [];

        foreach ($this->productRepository->findAll() as $product) {
            $data[] = $product->toArray();  // Samozřejmě ne nutně toArray, ale klidně jen vytažení některých věcí a odstranění závislostí na jiných entitách
        }

        Nette\Utils\FileSystem::write(
            'heurekaFeed.json',
            Nette\Utils\Json::encode($data),
        );
    }
}

A to ti prostě nikdo nemá jak obejít.

Prostě vždycky máš jako vstup do aplikace buď presenter, nebo při přístupu z CLI soubor jako je výše. Při CLI autorizaci nepotřebuješ a v ostatních jsou presentery. Takže shrnutí spíše je, že když se uživatel nedostane kam nemá už přes presenter, tak nic nepokazí. Tedy model může být bez aturorizace a tím je i přehlednější a hlavně autorizace je na jednom místě a při změně nemusíš hledat kde to vlastně máš. Taky když bude model čistý bez autorizace, tak ho můžeš přesouvat mezi projekty a autorizaci si budou řešit presentery.

Editoval Bulldog (21. 10. 2022 0:45)

m.brecher
Generous Backer | 758
+
-3
-

@Bulldog díky za ukázku výborně zabezpečených akcí presenteru, ano, když validuješ přímo v komponentě nejde to obejít žádnou neexistující akcí. Nicméně dobře provedená autorizace v modelu je také dobré řešení. Tu také není jak obejít, pokud se udělá dobře. Navíc je blíž k chráněným datům.

m.brecher
Generous Backer | 758
+
0
-

@Bulldog

Ahoj, koukám na ten kód a řekl bych, že skutečně nejde nikudy obejít:

class OrderPresenter extends Nette\Application\UI\Presenter
{
    #[Nette\DI\Attributes\Inject]
    public OrderRepository $orderRepository;
	private bool $orderEditRight = false;	// Když je nastavím na false, tak vím, že se tam nikdo nedostane, pokud to explicitně nepovolím
	private bool $orderAddRight = false;
	private Order $order;


    public function actionEdit(int $id)
    {
        $order = $this->orderRepository->getById($id);

        if (!$order) {
            $this->error('Not found', 404);
        }
        if (!$this->user->isAllowed($order, 'edit')) {
            $this->error('Not Allowed', 401);
        }

		$this->order = $order;
		$this->orderEditRight = true;
    }

    public function actionAdd()
    {
        if (!$this->user->isAllowed('order', 'add')) {
            $this->error('Not Allowed', 401);
        }
		$this->orderAddRight = true;
    }

	public function createComponentOrderEdit(): Form
	{
		if (!$this->orderEditRight) {
            $this->error('Not Allowed', 401);
        }
		... // samotná tvorba
	}

	public function createComponentOrderAdd(): Form
	{
		if (!$this->orderAddRight) {
            $this->error('Not Allowed', 401);
        }
		... // samotná tvorba
	}
}

Nicméně je tam poměrně dost řádků kódu, které nějak distribuují to oprávnění, které samo o sobě se skrývá v:

$this->user->isAllowed('order', 'add')

a

$this->user->isAllowed($order, 'edit')

Kdyby Jsi do modelu do metody editující Order přidal tyto dva řádky, ale volající zdrojový autorizátor, (ne $user) bylo by to úplně stejně zabezpečené a to i pro případy, kdyby se Order editoval ve více různých presenterech.

Rozhodně Ti nechci podsouvat, že to máš nějak nedokonale udělané, to ne, ale nějak nechápu, proč tak odmítáš i tu druhou možnost – validovat v modelu, když to je minimálně rovnocenné?

Předpokládám, že používáš zabezpečení integrity dat cizími klíči. To je také takové modelové pravidlo. Když nastavíš restrict pro delete i update, nemusíš se o mazání kategorie v presenteru starat (tak aby šlo smazat jen prázdnou kategorii), je to zabezpečené přímo v databázi, což je součást modelu. Presenter to neřeší, řeší to model, funguje to a používá se to.

m.brecher
Generous Backer | 758
+
0
-

@Bulldog

Já bych měl s kódem který zabezpečuje add a edit objednávek ještě jeden problém. Já mám vždycky společný formulář pro editaci i pro nový záznam. Navíc v editaci je u mě standardem, že má formulář i mazací tlačítko. Mazat můžeme třeba i z výpisu – jinou komponentou. Tam už si myslím je skutečně lepší validovat oprávnění (autorizovat) až v modelu. Kdyžtak pošlu nějaké kódy, ale myslím, že si člověk umí představit, jak by to bylo složité. Přitom v modelu je to tak názorné, tak jednoduché. V modelu máme jediné místo, kde se maže objednávka. V této mazací metodě na prvním řádku ověříme oprávnění, na dalších řádcích můžeme ověřovat splnění různých dalších podmínek a nakonec vykonat akci.

Bulldog
Člen | 110
+
0
-

Rozhodně Ti nechci podsouvat, že to máš nějak nedokonale udělané

v příkladu to rozhodně nedokonale je.
Jak jsem psal, tak reálně na to mám jedno místo (traitu), které vytváření komponent zakazuje/povoluje, tedy jakoby svážu v rámci presenteru komponenty pouze s určitými akcemi. Pak to mám na jednom místě a kód se scvrkne pouze na ověřování action.

proč tak odmítáš i tu druhou možnost

protože podle mě není správná. Porušuje spousty programátorských principů a zesložiťuje kód jak pro úpravy, údržbu, rozšiřování, tak pro přenositelnost. Všechno už tu bylo napsáno. Klidně si to pročti znovu.

Předpokládám, že používáš zabezpečení integrity dat cizími klíči.

Integrita dat a oprávnění s nimi nakládat jsou 2 úplně odlišné věci a tedy se o to starají odlišné místa.

Respektive Aplikace se dá připodobnit například k panelovému domu. (Sice ne moc šťastně zvoleno, ale službu to udělá). Hlavní vchod je zabezpečen zámkem. To je dejme tomu ověření, jestli je uživatel přihlášen. Pokud nejsi přihlášen, tak máš možnost vidět pouze dům zvenku. Pokud přihlášen jsi, můžeš vejít i dovnitř.
Zde ale je velká chodba a na ní je tuna pater (Presenterů) a každé patro má tunu dveří (Action) a když projdeš jednotlivými dveřmi, tak dostaneš pohled na to, jak vypadá který konkrétní byt (View) a můžeš komunikovat s obyvatli toho bytu (Model) a můžeš třeba číst knihy co tam mají (data). Tedy konkrétní dveře na daném patře (Presenter action) ti předají konkrétní vzhled bytu (View) který je spojen s konkrétními lidmi co se o ten byt starají (Modelem)
Pointa je ta, že všechny dveře (Actiony) jsou zamčené a ty musíš mít klíč (Práva) aby jsi ty dveře otevřel a pustili tě dovnitř. Jak vidíš, tak Presenter je ten, který se stará o přístup k dané stránce (bytu)..
Pokud ale mají 2 byty na stejném patře společný balkon (Komponenta) a balkonové dveře nejsou zabezpečeny (CreateComponent) tak můžeš easy z bytu kam máš přístup vejít do bytu, kam nemáš přístup. (Z jedné akce zavolat komponenty jiné akce.) Nebo někdo může na balkon vyšplhat zvenku a vejít balkonovkami dovnitř (Volání komponenty z neexistující akce)

Takže co uděláš? Na každé balkonové dveře dáš opět zámek, kterým projde jen ten, kdo má daná práva, která se shodují s právy dané akce (bytu)..

Dejme tomu ale teď tu integritu dat o které píšeš. Máš v jednom konkrétním bytě knihovnu. Kdo by měl ověřovat, že do té knihovny patří knihy a ne nádobí? Mají to dělat dveře? Ne Bude to dělat buď sama knihovna (databáze), pokud je dostatečně chytrá, nebo knihovník (model) a ideálně oba, protože po cestě po síti můžou být data odposlechnuty a změněny.
Takže knihovna (databáze) se stará jen o to, aby data do ní uložená byla ta data, která ona uložit umí a v tom formátu v jakém je uložit umí (nebudeš rvát CD do poličky dělané na WHS).
To, kdo tam ty data dává ale není starost knihovny.

Tím pádem můžeš v případě potřeby knihovnu přesouvat mezi byty a víš, že se k ní nikdy nedostane nikdo, kdo nemá přístup, protože o to se starají už dveře…

m.brecher
Generous Backer | 758
+
0
-

@Bulldog

Porušuje spousty programátorských principů a zesložiťuje kód jak pro úpravy, údržbu, rozšiřování, tak pro přenositelnost.

No nad tím jsem nepřemýšlel – úprava, údržba, rozšiřování, přenositelnost, to asi bezpochyby nebude úplně jednoduché. Je potřeba hledat v modelu cesty, jak to udělat, aby se autorizovalo neprůstřelně, ale aby to nenarušilo úpravy, údržbu, … Takže časem se ukáže. Příští rok budu mít jeden projekt se složitější autorizací, takže možná skončím u jednoakčních presenterů.

Editoval m.brecher (21. 10. 2022 3:04)

Bulldog
Člen | 110
+
0
-

Já bych měl s kódem který zabezpečuje add a edit objednávek ještě jeden problém. Já mám vždycky společný formulář pro editaci i pro nový záznam.

To přeci není problém a o to víc tento způsob oceníš. Protože nemusíš v modelu zvlášť ověřovat jestli zrovna uživatel edituje a má právo, nebo jestli uživatel vkládá a má právo, ale je ti to jedno. Prostě jednotně vložíš a je to. No a presenter? Ez:

class OrderPresenter extends Nette\Application\UI\Presenter
{
    #[Nette\DI\Attributes\Inject]
    public OrderRepository $orderRepository;
	private bool $orderFormRight = false;	// Když je nastavím na false, tak vím, že se tam nikdo nedostane, pokud to explicitně nepovolím
	private ?Order $order = null;


    public function actionEdit(int $id)
    {
        $order = $this->orderRepository->getById($id);

        if (!$order) {
            $this->error('Not found', 404);
        }
        if (!$this->user->isAllowed($order, 'edit')) {
            $this->error('Not Allowed', 401);
        }

		$this->order = $order;
		$this->orderFormRight = true;
    }

    public function actionAdd()
    {
        if (!$this->user->isAllowed('order', 'add')) {
            $this->error('Not Allowed', 401);
        }
		$this->orderFormRight = true;
    }

	public function actionDetail(int $id)
	{
		// tady řádek $this->orderFormRight = true; nebude, jelikož ze show akce nechceme upravovat ani přidávat články že..
		// Protože kdo může články vidět je nemusí nutně smět upravovat, nebo přidávat
	}

	public function createComponentOrderForm(): Form
	{
		if (!$this->orderFormRight) {
            $this->error('Not Allowed', 401);
        }
		... // samotná tvorba
	}
}

Navíc v editaci je u mě standardem, že má formulář i mazací tlačítko. Mazat můžeme třeba i z výpisu – jinou komponentou. Tam už si myslím je skutečně lepší validovat oprávnění (autorizovat) až v modelu. Kdyžtak pošlu nějaké kódy, ale myslím, že si člověk umí představit, jak by to bylo složité.

Jasně. Dodáme mazání jedním z možných způsobů:

class OrderPresenter extends Nette\Application\UI\Presenter
{
    #[Nette\DI\Attributes\Inject]
    public OrderRepository $orderRepository;
	private bool $orderFormRight = false;	// Když je nastavím na false, tak vím, že se tam nikdo nedostane, pokud to explicitně nepovolím
	private ?Order $order = null;


    public function actionEdit(int $id)
    {
        $order = $this->orderRepository->getById($id);

        if (!$order) {
            $this->error('Not found', 404);
        }
        if (!$this->user->isAllowed($order, 'edit')) {
            $this->error('Not Allowed', 401);
        }

		$this->order = $order;
		$this->orderFormRight = true;
    }

    public function actionAdd()
    {
        if (!$this->user->isAllowed('order', 'add')) {
            $this->error('Not Allowed', 401);
        }
		$this->orderFormRight = true;
    }

	public function actionDetail(int $id)
	{
		// tady řádek $this->orderFormRight = true; nebude, jelikož ze show akce nechceme upravovat ani přidávat články že..
		// Protože kdo může články vidět je nemusí nutně smět upravovat, nebo přidávat
	}

	public function actionGrid()
	{
		// tady řádek $this->orderFormRight = true; taky nebude...
	}

	public function createComponentOrderForm(): Form
	{
		if (!$this->orderFormRight) {
            $this->error('Not Allowed', 401);
        }
		... // samotná tvorba
	}

	public function handleDelete(int $id)
	{
		// Ověřit existenci a vyhodit 404 v případě neexistence stejně musíme, takže to v presenteru budeme tahat
		$order = $this->orderRepository->getById($id);

        if (!$order) {
            $this->error('Not found', 404);
        }

		// a když už tady máme tu objednávku, která není jen ID, ale i data o vlastníkovu atp., vož potřebujem
		// k úspěšné autorizaci, tak to rovnou ověříme ne?
		if (!$this->user->isAllowed($order, 'delete')) {
            $this->error('Not Allowed', 401);
        }

		// jupí. tady můžeme mazat
		$this->orderRepository->delete($order);

		// A přesměrujeme vždy na grid, protože víme, že pokud má uživatel právo mazat, tak musí mít i právo na prohlížení
		// Takže pokud jsme v detailu, tak po smazání detail neexistuje a musíme redirectovat jinam a grid je super volba
		// a pokud jsme na gridu, kde je druhé mazací tlačítko, tak opět redirectujeme na akci grid.
		$this->redirect('grid');
	}
}

Všimni si, že v této handle metodě delete nemusím řešit z jaké akce to kdo poslal.
Důvod je jednoduchý.

  1. Pokud je to z akce, kde tlačítko smazat je, tak super. Nemusím nic řešit.
  2. Pokud je to z akce, kde tlačítko smazat není, tak stejně ověřuju oprávnění, jestli můžu mazat tento konkrétní článek a pokud ne, tak mě to dál stejně nepustí
  3. Tedy mazat články, které mohu mazat můžu odkudkoliv, ale jen ty které mohu mazat a upřímně, pokud můžu mazat jen své články, tak proč bych aplikaci hackoval abych je mohl mazat z přidání, když v jiných akcích na to mám tlačítka ne? :D

EDIT
Mluvím o článcích ale v kódu jsou objednávky. Sorry :D Ospalost se občas projeví.

Editoval Bulldog (21. 10. 2022 3:20)

Bulldog
Člen | 110
+
0
-

Navíc pokud budeš chtít mazat ve více komponentách a nebudeš chtít mít tu mazací akci v Presenteru, co ti brání udělat si takovou traitu a tu ve všech komponentách, kde chceš mazat používat? (Je totiž obecná na mazání kdekoliv a čehokoliv.)

trait SecureDelete
{
	private Repository $repository; 	// Rodič repositářů do kterého vejdou všichni.
	private Nette\Security\User $user; 	// User nad kterým ověřuješ práva
	private array $onDelete = [];		// Callbacky třeba s přesměrováním z presenteru

	public function handleDelete(int $id): void
	{
		$entity = $this->repository->getById($id);	// Entity implementuje Nette\Security\Resource

        if (!$entity) {
            $this->error('Not found', 404);	// Ano error metoda už je i v komponentách
        }

		if (!$this->user->isAllowed($entity, 'delete')) {
            $this->error('Not Allowed', 401);
        }

		$this->repository->delete($entity);

		$this->redrawControl('data'); // ?? Klidně může být nahrazeno v těch callbackách :D
		\Nette\Utils\Arrays::invoke($this->onDelete);
	}
}

Editoval Bulldog (21. 10. 2022 3:18)