Správné DI, model přístupný z více míst

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
PVD
Člen | 20
+
0
-

Zdravím,

v Nette jsem začátečník a snažím se postupovat podle posledních trendů. Řeším modelovou situaci:

Mám třídu ConfigManager (dále CM), která má metody getXXX() pro získání určité hodnoty z vlastního configu aplikace. Momentálně je bere z DB z tabulky prefix_config. Používám ji jako takový primitivní model pro nepřímý přístup k hodnotám v dané tabulce s tím, že časem to třeba změním na čtení ze souboru. Dál si to budu moct odděleně upravit, používat mappery apod.

  1. Třída je registrovaná jako služba
<?php
services:
	config: App\Model\ConfigManager
?>
  1. V BasePresenteru ji injectuji přímo
<?php
	/** @var \App\Model\ConfigManager @inject */
	public $config;
?>

Řeším však jak správně použít CM bez tvoření nových instancí (což mi připadá jako nesprávné řešení) i na jiných místech, kde inject nefunguje (alespoň stejný zápis jako v Presenteru se mi nepodařilo rozchodit).

  1. UserManager ze sandboxu – CM jsem získal v konstruktoru – public function __construct(Nette\Database\Context $database, ConfigManager $config)
  2. třída Passwords, která se stará o ověření hesla a jeho zahashování, má dvě statické metody hash() a verify() – jako funkční a vcelku smysluplné řešení (ačkoliv mi nepřipadá ideální) jsem vytvořil (statickou) metodu setToken(), aby mohla nastavit statickou proměnou $token, která je použita ve statické funkci hash():
<?php
class Passwords
{
	private static $token;

	public static function hash($username, $password)
	{
		...
		self::$token
		...
		return $hash;
	}

	public static function setToken($token){
		self::$token = $token;
	}
?>

Tato metoda je volána při autorizaci, aby měla třída Passwords vždy aktuální token při ověřování, avšak aby ho nedostávala ve všech metodách v parametrech (kromě hash() a verify() mohou být další, ty všechny se postupně volají).
Teoreticky bych však mohl i zde potřebovat šáhnout na nějakou metodu CM.

  1. LoginFactory obsahuje metodu formSucceeded (opět ze sandboxu), tam se nastavuje setExpiration(), i zde bych rád použil metodu z CM. CM jsem opět získal v konstruktoru, obdobně jako u UserManagera:
<?php
...
	/** @var ConfigManager */
	private $config;

	public function __construct(User $user, ConfigManager $config)
...
?>

Otázky:

  1. je mé řešení injektováním v konstruktoru správné?
  2. jak to řešit lépe?
  3. jak získat stejným způsobem CM i v Passwords pro použití ve statických metodách?
  4. je něco z toho, co píši a jak uvažuji špatně?
  5. chápu to správně, že když je třída CM registrována jako služba, tak se vytvoří její instance pouze jednou a pak se tato instance injektuje tam, kde je potřeba k ní mít přístup?

Kritiku uvítám, potřebuji ji.

Mockrát děkuji komukoliv za odpověď a čas, který mi věnuje.

Jan Tvrdík
Nette guru | 2595
+
0
-

Třída Passwords by neměla být statická, token by se měl normálně předávat přes konstruktor.

newPOPE
Člen | 648
+
0
-
  1. ano
  2. „constructor injection“ je v podstate na prvom mieste zo zoznamu moznosti injektovania
  3. v staticky metodach? Na tie sa vykasli a pouzivaj dynamiku
  4. blbe je len to ze uvazujes o statickych metodach. Statika je v poriadku akurat musis vediet kde a kedy (napr. taka Library class ktora nema stav)
  5. ano mala by sa
PVD
Člen | 20
+
0
-

Moc děkuji za tak rychlé komentáře, to jsem nečekal.
Třídu Passwords jsem udělal dle originální Nette\Security\Passwords, která měla metody statické, myslel jsem, že to má nějaký význam.

Když nebude mít statické metody, mám ponechat jejich volání

<?php
...
elseif (!Passwords::verify($username, $password, $row[self::COLUMN_PASSWORD_HASH])) {
...
?>

nebo mám na začátku autentikace vytvořit její instanci (v konstruktoru předat token nebo config) a tu pak používat pro dané metody? Registrovat to jako službu mi připadá zbytečné, když je potřeba jen při přihlašování.

Každopádně když to shrnu, tak v BasePresenteru nechám inject jak je, všude jinde budu dávat CM přes konstruktor. V případě Passwords zvážím, zda potřebuje mít celý CM, nebo jen ten token.

@newPOPE: můžeš mě, prosím, nasměrovat třeba na nějaký zdroj, kde mohu nastudovat více o tom, kdy použít static metody, jak říkáš třeba příklad Library class?

Oběma ještě jednou velké díky!

Šaman
Člen | 2666
+
0
-

Jenže Nette\Security\Passwords nemá žádné stavy a jen simuluje na nižších verzích PHP funkce, které byly do PHP přidané až od 5.5
Pokud máš vyšší PHP, tak je lepší používat core funkce.

A bez urážky, za self::$token = $token; bys měl dostat domácí úkol zopakovat si základy OOP. :)

Editoval Šaman (10. 7. 2015 12:09)

Unlink
Člen | 298
+
0
-

A načo si robíš vlastnú triedu na hashovanie hesiel?

David Grudl
Nette Core | 8239
+
+5
-

Šaman napsal(a):

Pokud máš vyšší PHP, tak je lepší používat core funkce.

Core funkce password_hash v případě selhání vrátí FALSE, což pokud neověříš a uložíš do databáze, uloží se ti tam prázdný řetězec. Nette\Security\Passwords::hash() vyhodí výjimku.

Šaman
Člen | 2666
+
0
-

David Grudl napsal(a):

Šaman napsal(a):

Pokud máš vyšší PHP, tak je lepší používat core funkce.

Core funkce password_hash v případě selhání vrátí FALSE, což pokud neověříš a uložíš do databáze, uloží se ti tam prázdný řetězec. Nette\Security\Passwords::hash() vyhodí výjimku.

Aha, tak když už jsme ten offtopic nakousli, tak tyhle funkce plánuješ mit v Nette nastálo, nejen do té doby, dokud nebude ukončená podpora pro PHP 5.4? Myslel jsem, že jsou jen proto, aby upgrade na PHP >=5.5 znamenal jen fulltextově nahradit Passwords::verify za password_verify a vymazat Nette\Security\Passwords v sekci use. ??

Editoval Šaman (10. 7. 2015 12:14)

David Grudl
Nette Core | 8239
+
0
-

Uvidíme.

newPOPE
Člen | 648
+
0
-

PVD napsal(a):

Presne zdroje nemam (niekde som o tom cital). Ale library class je len nejaka sada funkcii zabalena do triedy a jej metod. Napr Strings v Nette a tak podobne. Ono tam nie je o com moc spekulovat :)

PVD
Člen | 20
+
0
-

Šaman napsal(a):

Jenže Nette\Security\Passwords nemá žádné stavy a jen simuluje na nižších verzích PHP funkce, které byly do PHP přidané až od 5.5
Pokud máš vyšší PHP, tak je lepší používat core funkce.

A bez urážky, za self::$token = $token; bys měl dostat domácí úkol zopakovat si základy OOP. :)

Že nemá Nette\Security\Passwords stavy chápu, ale nechápu, v čem je rozdíl u mě, když ji používám stejně, jen jsem jádro metod nahradil svým způsobem hashování?

Základy OOP rád zopakuji a souhlasím, že je to prasárna. Zkoušel jsem, jaké existuje řešení při zachování statických funkcí a sám jsem psal, že mi to nepřijde ideální. :) Proto jsem raději poprosil o radu.

@Unlink Dělám ji z důvodu jiného způsobu hashování hesel – výchozí způsob zabezpečení je sice výborný, ale momentálně je požadavek na nový systém zachovat starý způsob, aby nebyla hesla zbytečně zneaktivněna (alespoň dočasně, než dojde k vynucení změny hesla). Rozhodně se ale nejedná o nějaké jednoduché použití md5.

Unlink
Člen | 298
+
0
-

A používaš to hashovanie hesiel na viacerých miestach?
Možno by stačilo ho dať priamo do authorizátora…

Druhá možnosť je, že keď to potrebuješ vo vlastnej triede, tak by som si do tej metody hash pridal ten parameter token
public static function hash($username, $password, $token)

Tretia možnosť (zrejme najlepšia) sprav tu triedu ako službu tj.
metódy nebudú statické, cez konštruktor si tam injectneš ten config manager, zaregistruješ v configu, a potom tam kde to budeš potrebovať si to injectneš a budeš používať.

PVD
Člen | 20
+
0
-

Unlink napsal(a):

A používaš to hashovanie hesiel na viacerých miestach?
Možno by stačilo ho dať priamo do authorizátora…

Druhá možnosť je, že keď to potrebuješ vo vlastnej triede, tak by som si do tej metody hash pridal ten parameter token
public static function hash($username, $password, $token)

Tretia možnosť (zrejme najlepšia) sprav tu triedu ako službu tj.
metódy nebudú statické, cez konštruktor si tam injectneš ten config manager, zaregistruješ v configu, a potom tam kde to budeš potrebovať si to injectneš a budeš používať.

  1. zatím nepoužívám, ale předpokládám, že budu, třeba při generování nového hashe, změně hesla apod.
  2. parametr bych musel ale přidat i do verify, jelikož jednou volám přímo hash, jednou verify, uplatnění je různé, proto jsem se chtěl tomuto vyhnout
  3. jako služba by se však registrovala vždy, což jsem nechtěl, jelikož se používá jen občas (oproti tomu CM)

Momentálně jsem změnil statické metody na normální a při autentizaci, když potřebuji používat její fce, si vytvořím instanci a z ní je používám/volám. Tím pádem není třída registrována vždy, ale jen když je potřeba. No a v konstruktoru mám rovnou token, protože zatím nepotřebuji nic jiného, než ten token, když se to změní, injektnu klidně přímo CM.

jiri.pudil
Nette Blogger | 1032
+
0
-

Ad 3. Jako služba se sice zaregistruje, ale reálně se vytvoří, až když je potřeba, takže bych v tom neviděl problém.

PVD
Člen | 20
+
0
-

jiri.pudil napsal(a):

Ad 3. Jako služba se sice zaregistruje, ale reálně se vytvoří, až když je potřeba, takže bych v tom neviděl problém.

Pravda, na to jsem zapomněl. Díky.

Každopádně předpokládám, že je správnost registrace jako službu a vytvoření nové instance tam, kde je třeba stejná, nebo ne?

Editoval PVD (11. 7. 2015 11:06)

Unlink
Člen | 298
+
0
-

To aby si mal len jednu inštanciu nieje podľa mňa hlavný dôvod prečo používať služby.

A celkovo, z tých možností čo som ti navrhol, je táto asi najlepšia, ak to plánuješ používať na viacerých miestach, proste tu logiku okolo generovania hesla budeš mať zaobalenú v službe, ktorá sa bude o ňu starať.

Šaman
Člen | 2666
+
0
-

PVD napsal(a):

Šaman napsal(a):

A bez urážky, za self::$token = $token; bys měl dostat domácí úkol zopakovat si základy OOP. :)

Základy OOP rád zopakuji a souhlasím, že je to prasárna. Zkoušel jsem, jaké existuje řešení při zachování statických funkcí a sám jsem psal, že mi to nepřijde ideální. :) Proto jsem raději poprosil o radu.

Ale tady nejde o to, že by to byla to prasárna. To bych se ti snažil vysvětlit proč je tomu tak. Problém je, že ti to nefunguje. Není možné nastavovat statickou property. Zkus si to dumpnout. Ten self::$token je stále NULL, ať ho nastavuješ jak chceš. Jenom mě udivuje, že PHP nevyhodilo žádnou hlášku, že děláš nemožnou operaci. Tím zopakováním OOP jsem myslel právě omezení statických propert a metod a jejich použití.

Statické metody musí dostat všechny parametry při zavolání, takže pokud na statice opravdu trváš, tak jediná možnost je ta, která psal @Unlink.</del>


WTF? Zkusil jsem ti k tomu najít nějaké podklady a vypadá to, že se statická property nastavovat dá. Dokonce mi to funguje, když to zkouším. Takže se omlouvám, základy OOP jsem si zopakoval já. Nicméně našel jsem jediný příklad použití a to je singleton (který byl zlo, ale po Davidově vymítání už po něm pes neštěkne).

Takže jestli ti to funguje, tak už raději nic nepíšu, ale jestli chceš přece jen postupovat podle Nette best practise (tedy ty poslední trendy, jak pišeš na začátku), tak si celou třídu Passwords udělej dynamickou, zaregistruj si ji v configu jako službu a pracuj s ní jako s běžnou službou. Ten token pak můžes nastavovat buď v konstruktoru té třídy (pokud je to nutná podmínka, aby třída fungovala). Nastavit ho pak můžeš přímo v configu, takže ten, kdo si vyžádá službu Passwords ji dostane rovnou připravenou k použití a nemusí řešit, kde vezme token. Pokud by se token měnil pro každý výpočet, pak ho předávej jako parametr přímo do funkce.

PVD
Člen | 20
+
0
-

Díky oběma. :)

@Unlink Jde mi spíš o to, zda se v obou případech tvoří instance shodně jen jednou. Výhodu služby u Passwords zvážím, zatím to ale nechám vytvořené jako novou instanci tam, kde to potřebuji (zatím jedno místo). Nechci si zaplácat config samými službami.

@Šaman Ano, jde to, kdyby mi to nefungovalo, tak jsem to napsal. :D Princip mé otázky na začátku byl ten, že jsem sice našel způsob, jak to funguje, ale chtěl jsem to udělat co nejlépe.
Tudíž jak píšeš a jak radili ostatní, Passwords je nyní dynamická. Zatím ji ale nemám jako službu, viz odpověď Unlinkovi (omlouvám se, pokud se to nemá skloňovat :) ).

Jsem každopádně rád, že nejsem úplně mimo, zatím… :)

Šaman
Člen | 2666
+
0
-

Kdy a jak jí vytvářet, to už je problém DI kontejneru. Tedy nejvyšší autority, která vytváři služby a předává je dalším službám, které vytváří. Toho, že bys měl plný config služeb, toho se neboj, to je normální. Je lepší mít všechny služby v DI kontejneru, než si je vytvářet někde sám ručně. Třeba i proto, že pokud bude potřeba i na jiném místě (viz název vlákna), tak si buď vytvořiš novou instanci (a máš dvě, klidně s různým tokenem, super zdroj bugů), nebo si musíš pamatovat, že jsi ji už někde vytvářel a snažit se získat tu jednu (což nemusí být možné). A třetí možnost je že pak to přepíšeš tak, jak ti radíme (tak proč to neudělat hned na začátku pořádně). Tedy že si pomocí zápisu v configu vytvoříš službu v DI kontejneru a z něho si ji vyžádá kdokoliv ji bude potřebovat.

PVD
Člen | 20
+
0
-

Šaman napsal(a):

Kdy a jak jí vytvářet, to už je problém DI kontejneru. Tedy nejvyšší autority, která vytváři služby a předává je dalším službám, které vytváří. Toho, že bys měl plný config služeb, toho se neboj, to je normální. Je lepší mít všechny služby v DI kontejneru, než si je vytvářet někde sám ručně. Třeba i proto, že pokud bude potřeba i na jiném místě (viz název vlákna), tak si buď vytvořiš novou instanci (a máš dvě, klidně s různým tokenem, super zdroj bugů), nebo si musíš pamatovat, že jsi ji už někde vytvářel a snažit se získat tu jednu (což nemusí být možné). A třetí možnost je že pak to přepíšeš tak, jak ti radíme (tak proč to neudělat hned na začátku pořádně). Tedy že si pomocí zápisu v configu vytvoříš službu v DI kontejneru a z něho si ji vyžádá kdokoliv ji bude potřebovat.

Díky za rady. Máš pravdu, přehlednost na první místě a když jsou vytvářeny jen v případě potřeby, je to luxus a proč bych ho neměl využít. Dám to jako službu a bude to.

Díky všem za komentáře a pomoc.

Edit:
Přecijen mi ještě jedna věc hapruje – když registruji Passwords jako službu, tak si ji injektnu v konstruktoru UserManagera. Avšak to je zbytečné, protože ji potřebuji až při provádění určité metody (třeba authenticate). Jak ji mohu injektnout později? Pokud vytvořením instance, tak postrádá registrování jako službu smysl, ne?

Editoval PVD (11. 7. 2015 21:50)

Unlink
Člen | 298
+
0
-

No na to sú v princípe dve možnosti

1.
použitie accessorov (neviem či je to niekde popísané, ale ide o to že)
vytvoríš si interface

interface IPasswordsAccessor
{
	/** @return Passwords */
	function get();
}

ktorý bude mať jednu metódu get() a return bude tá tvoja trieda

- create: Passwords
  implement: IPasswordsAccessor

a v konštruktore si injectneš IPasswordsAccessor a tam kde budeš potrebovať tú tvoju triedu si zavoláš get
Ale treba to používať len ak sa to oplatí (teda tá služba je náročná na vytvorenie a nepotrebuješ ju vždy)

A teraz mi došlo, že druhá možnosť je mágia a je možná len v presenteroch.

Ale k tej tvojej otázke, tým že to injectuješ cez konštruktor v podstate deklaruješ že služba UserManager potrebuje ku svojej činnosti službu Passwords.

Otázka je ale, či trieda UserManager má mať na zodpovednosti hashovanie hesiel, alebo by to mal mať na starosti niekto iný (napr. Authorizator).

PVD
Člen | 20
+
0
-

Unlink napsal(a):

No na to sú v princípe dve možnosti

1.
použitie accessorov (neviem či je to niekde popísané, ale ide o to že)
vytvoríš si interface

ktorý bude mať jednu metódu get() a return bude tá tvoja trieda

a v konštruktore si injectneš IPasswordsAccessor a tam kde budeš potrebovať tú tvoju triedu si zavoláš get
Ale treba to používať len ak sa to oplatí (teda tá služba je náročná na vytvorenie a nepotrebuješ ju vždy)

Předpokládám, že v mém případě se to moc nevyplatí…

A teraz mi došlo, že druhá možnosť je mágia a je možná len v presenteroch.

Ale k tej tvojej otázke, tým že to injectuješ cez konštruktor v podstate deklaruješ že služba UserManager potrebuje ku svojej činnosti službu Passwords.

To je právě to, proč jsem to nechtěl takto deklarovat, já službu Passwords potřebuji pouze v určitých metodách, ale UserManager je registrován vždy.

Otázka je ale, či trieda UserManager má mať na zodpovednosti hashovanie hesiel, alebo by to mal mať na starosti niekto iný (napr. Authorizator).

To je hodně zajímavá otázka a bohužel nevím, jak si na ní odpovědět. V principu UserManager tuto zodpovědnost nemá, tu má Passwords a tak ji chci i zachovat. Jde jen o to, že chci do Passwords dostat určitou hodnotu CM (momentálně jen ten token). Ten token mohu předat v konstruktoru Passwords a vytvořit si instanci v UserManager tehdy, když ji potřebuji. To jsem však i díky komentáři @Šamana zavrhnul, aby to bylo lehce upravitelné. V momentě, kdy Passwords registruji jako službu, vytvoří se instance jen tehdy, když je potřeba. Jenže když ji injektnu v konstruktoru UserManager, vytvářím ji vždy – třeba i v momentě, kdy se zobrazuje „jen“ login formulář a přitom je potřeba až při odeslání login formuláře.

V mém případě není vytvoření instance Passwords náročné (nic v konstruktoru nedělám), takže k zpomalení moc nedojde. Chci však již od začátku svého projektu dělat práci pořádně a ať nevznikají zbytečné zvýšené nároky, které bych musel potom zpětně optimalizovat.

Unlink
Člen | 298
+
0
-

Chci však již od začátku svého projektu dělat práci pořádně a ať nevznikají zbytečné zvýšené nároky, které bych musel potom zpětně optimalizovat.

Myslím že toto použitie je v poriadku, ak sa ti to nepozdáva tak potom jedine delegovať túto zodpovednosť na authorizator, nejakú registration service…

PVD
Člen | 20
+
0
-

Unlink napsal(a):

Chci však již od začátku svého projektu dělat práci pořádně a ať nevznikají zbytečné zvýšené nároky, které bych musel potom zpětně optimalizovat.

Myslím že toto použitie je v poriadku, ak sa ti to nepozdáva tak potom jedine delegovať túto zodpovednosť na authorizator, nejakú registration service…

Myslíš použití jako registrovanou službu a injektování v konstruktoru UserManager, ačkoliv to potřebuji až v určité metodě, chápu správně? Protože je to malá třída s minimální náročností…

Díky!

Šaman
Člen | 2666
+
+1
-

Nevím, jak náročný projekt děláš, ale většinou takto specifická optimalizace není nutná. Největší režii má stejně připojení k databázi a přenost dat (jak z db, tak mezi serverem a klientem). Pokud je nutná optimalizace, provádí se až nakonec. Výpočetní výkon serverů je dnes na takové úrovni, že ani u středně velkých projektů není potřeba řešit rychlost (resp. rychlost i využití paměti závisejí vpodstatě jen na efektivitě práce s daty, nikoliv na režii jednotlivých služeb).
Pokud bys opravdu potřeboval řešit efektivitu za každou cenu, pak je vhodně třeba na kritické výpočty úplně opustit PHP a mít části modelu třeba v C, kde máš daleko větší kontrolu nad výpočtem (sám přiděluješ paměť) a navíc je to kompilovaný jazyk, takže spouštíš už serverovou aplikaci, nikoliv jen sadu instrukcí, kterou musí někdo interpretovat (PHP). I takovou aplikaci jsem viděl, ale i v této aplikaci se o presentační část staralo PHP a Nette a v C servletech byl jen model (resp. kritické části). Co se získáváni dat týče, tak většina kritických výpočtů se přesunula do stored procedur do databáze. Pokud použiješ lepší databázový engine, tak můžeš mít velmi vysokou kontrolu jak efektivně ti funguje třeba samotné vyhledávání, můžeš si psát vlastní okénkové funkce apod.
Další věc je, že celé OOP je náročnější na zdroje a výkon, než procedurální programování (máš režii s objekty, řeší se hierachie apod.), takže proto C, nikoliv C++. Ale OOP dělá program daleko čitelnější a lépe udržovatelný.
Další možností optimalizace je využití více vláken. Efektivnější databáze (než MySQL), vlastně úplné vynechání databázové vrstvy a psaní nativních query, … Je toho spousta, ale všechny cesty vedou spíše k obětování čistoty a přehlednosti, aby se posílila efektivita.
Mimochodem samotné Nette má poměrně velké množství tříd, které si vytvoří (viz. debug bar a DI container panel). Není to nic, co by reálně zpomalovalo, ale je to okolo dvaceti tříd.

Takže když už jsi se rozhodl pro neefektivní jazyk, pro používání OOP, pro komfort frameworku, pro databázovou vrstvu a jen běžný databázový engine, tak už toho využívej a snaž se psát čistě, přehledně, udržovatelně. Až bude potřeba optimalizovat, tak zjisti kde (a s největší pravděpodobností to bude indexováni databáze, případně zefektivnit algoritmus/datovou strukturu pro práci s velkými tabulkami).


Abych to shrnul – pokud se snažíš o přehledný a čistý objektový kód, je to kompromis na úkor efektivity. Ale z praktického a ekonomického hlediska je dnes pro majitele aplikace jednodušší a levnější zaplatit výkonnější hosting, než udržovat aplikaci v kombinaci procedurální PHP + C servlety + halda stored procedur a nativních query třeba v Oracle. (A když už by se optimalizovalo takhle extrémě, tak tam to PHP asi nemá co dělat).
Dokud nemáš nějakou opravdu náročnou třídu (která už nejde rozbít na menší), tak opravdu není potřeba řešit accessory a vytvářet komplikovanější řešení na úkor jednoduchosti a přehlednosti.

PVD
Člen | 20
+
0
-

@Šaman Máš pravdu, děkuji za hezký soupis. Projekt je náročný hlavně na DB a tam se prováděly už mnohokrát optimalizace. Třeba přechod na INNO_DB znamenalo výrazné zpomalení apod. To není v mé režii.

Můj úkol je víceméně předělat starou aplikaci z „čistého“ PHP (ve skutečnosti nabalovaný nepřehledný kód) do kvalitního MVC frameworku s bonusem, který jsem nabídnul, což je modul FORMS a jiné.

Každopádně jsem rád, že tak nějak vím, na čem jsem.

Ohledně optimalizace DB, narazil jsem na něco, co mě dost zarazilo, řeším to v jiné diskusi:
https://forum.nette.org/…unacteni-dat

Takže i v tom určitě budu optimalizovat.

Jinak mám pocit, že to registrování služeb + injektování bude silně návykové… :D