Správné DI, model přístupný z více míst
- PVD
- Člen | 20
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.
- Třída je registrovaná jako služba
<?php
services:
config: App\Model\ConfigManager
?>
- 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).
UserManager
ze sandboxu – CM jsem získal v konstruktoru –public function __construct(Nette\Database\Context $database, ConfigManager $config)
- třída
Passwords
, která se stará o ověření hesla a jeho zahashování, má dvě statické metodyhash()
averify()
– jako funkční a vcelku smysluplné řešení (ačkoliv mi nepřipadá ideální) jsem vytvořil (statickou) metodusetToken()
, aby mohla nastavit statickou proměnou$token
, která je použita ve statické funkcihash()
:
<?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.
LoginFactory
obsahuje metoduformSucceeded
(opět ze sandboxu), tam se nastavujesetExpiration()
, i zde bych rád použil metodu z CM. CM jsem opět získal v konstruktoru, obdobně jako uUserManagera
:
<?php
...
/** @var ConfigManager */
private $config;
public function __construct(User $user, ConfigManager $config)
...
?>
Otázky:
- je mé řešení injektováním v konstruktoru správné?
- jak to řešit lépe?
- jak získat stejným způsobem CM i v Passwords pro použití ve statických metodách?
- je něco z toho, co píši a jak uvažuji špatně?
- 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
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
- ano
- „constructor injection“ je v podstate na prvom mieste zo zoznamu moznosti injektovania
- v staticky metodach? Na tie sa vykasli a pouzivaj dynamiku
- 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) - ano mala by sa
- PVD
- Člen | 20
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
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)
- David Grudl
- Nette Core | 8239
Š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
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)
- PVD
- Člen | 20
Š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
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
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ť.
- 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.
- 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
- 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
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
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
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
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
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
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
Š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
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
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 interfacektorý 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žbuPasswords
.
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
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
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
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
@Š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