Observer v Nette – mám správný návrh?
- svezij
- Člen | 69
Ahojte, v mé aplikaci bych si chtěl implementovat návrhový vzor observer, ale chtěl bych si nejprve zjistit, zda si myslíte, že to tímhle způsobem jde a můj návrh není proti nějakým principům – případně mi z toho pomohli ven.
Předem se omlouvám, kdybyste našli nějaké syntaktické chyby v kódu,
vymýšlím a píši jej právě teď.
Hned zpočátku mám první dotaz. V modelu mám objekt, který by měl být
jak subject, tak observer, není už tohle špatně? Je to objekt
SecurityEngine extends Nette\Object implements NS\IAuthenticator, NS\IAuthorizator, IObservable, IObserver
a je to z toho důvodu, že by měl:
- logovat akce uživatele – odpovídat na události vyvolané prezentery (zde se jedná o observer)
- procházet všechny Observery a posílat jim zprávu o události (zde je to subject)
Všechny prezentery by vlastně měly být observery a provede-li „aktér“ systému nějakou akci, odešle se událost security enginu – událost plánuji ve tvaru metody base prezenteru:
/**
* Causes event call.
*
* @param string $action
* @param array $attributes
*/
public function event($action, array $attributes = array())
{
array_unshift($attributes, $this);
$this->securityEngine->event($action, $attributes);
}
V security enginu by pak byla obsluha:
/**
* Notifies observers about event.
*
* @param string $action
* @param array $attributes
*/
public function event($action, array $attributes)
{
$this->action = $action;
$this->attributes = $attributes;
$this->notify();
}
/**
* Handles event.
*
* @param string $action
* @param array $attributes
*/
public function notify()
{
if ($this->isLoggable($this->action)) {
$this->logUserAction();
}
foreach ($this->observers as $observer) {
$observer->handleEvent($this);
}
}
a prezentery samotné by pak měly obsluhu těchto akcí. Base prezenter by
implementoval IObserver
a tudíž by měl definovanou metodu
handleEvent
:
/**
* Handles event.
*
* @param IObservable $subject
*/
public function handleEvent(IObservable $subject)
{
$method = 'on' . ucfirst($subject->getAction());
if (method_exists($this, $method)) {
call_user_func_array(array($this, $method), $subject->getAttributes());
}
}
Je zde nějaká zásadní chyba? Mám něco udělat jinak nebo to tak může být?
Děkuji.
- frosty22
- Člen | 373
Zda-li je to proti nějakým principům bych asi neřekl. Pouze tedy to vypadá, že tvůj objekt SecurityEngine toho dělat nějak moc, když už implementuje jak Authenticator, tak i Authorizator a ještě si hrát na Observer – minimálně tohle bych rozdělil. Myslím že není problém, aby měl závislosti na dalších objektech Authenticator, Authorizator.
A potom tedy spíše se mi to zdá jako trošku zbytečné – tedy nevím, jaký je konkrétní cíl tohoto, ale Presentery dědí od Nette\Object, který podporuje události, takže je možné tam mít:
<?php
class FooPresenter extends Presenter {
public $onMojeAkce = array();
public function actionBar()
{
$this->onMojeAkce($this, $this->params); // ...
}
}
?>
A potom už události můžeš navěšovat případně se podívej na Kdyby\Events http://filip-prochazka.com/…te-framework
A pokud jde o zabezpečení jednotlivých handle*, action* metod presenterů, tak na to by se ti mohla hodit metoda presenteru checkRequirenments(Reflector $reflector)
- svezij
- Člen | 69
Zda-li je to proti nějakým principům bych asi neřekl. Pouze tedy to vypadá, že tvůj objekt
SecurityEngine toho dělat nějak moc, když už implementuje jak Authenticator,
tak i Authorizator a ještě si hrát na Observer – minimálně tohle bych rozdělil.
Myslím že není problém, aby měl závislosti na dalších objektech Authenticator, Authorizator.
Jj, to je pravda, SecurityEngine
se zdá univerzální,takže
pokud implementuji vlastní observer vzor, udělám to odděleně od objektu
SecurityEngine
, děkuji.
A potom tedy spíše se mi to zdá jako trošku zbytečné – tedy nevím, jaký je
konkrétní cíl tohoto, ale Presentery dědí od Nette\Object, který podporuje události
To je také pravda a chtěl jsem o tom uvažovat, ale jestli jsem to dobře pochopil, tak bych jednak musel vytvořit pro každou událost property, navíc nevím, jak by se dala zařídit obsluha událostí více posluchači. Potřebuji např. při změně stavu objednávky:
- zalogovat, že uživatel změnil stav objednávky
- jedním posluchačem odchytit tuhle událost a odeslat e-mail zákazníkovi
- druhým posluchačem odchytit tuhle událost a změnit skladovou dostupnost
- dalším posluchačem udělat tohle a onohle
Ale ne vždycky budou mít posluchači přístup k pozorovateli. A i kdyby
ano (tzn. použil bych např. můj SecurityEngine
), tak budu muset
v poli $this->onEvent
nějak rozeznat, kterou mám obsluhovat,
protože já nepotřebuji na jednu akci zavolat všech x metod z tohoto pole,
ale jen některé. No… tohle se mi zdá příliš komplikované a bohužel ne
moc použitelné.
A potom už události můžeš navěšovat případně se podívej na Kdyby\Events
To vypadá jako přesně to, co potřebuji.
A pokud jde o zabezpečení jednotlivých handle*, action* metod presenterů,
tak na to by se ti mohla hodit metoda presenteru checkRequirenments(Reflector $reflector)
Nejde, zabezpečeno to mám trochu jinak, ale děkuji za metodu, kterou možná časem také využiji.
Každopádně mockrát děkuji, kouknu na Kdyby\Events, vypadá to dobře a
případně si to implementuji s oddělenou implementací od
SecurityEngine
.
- svezij
- Člen | 69
Zdravím ještě jednou, chtěl jsem vyzkoušet Kdyby\Events, ale pomalu
zjišťuji, že největší problémy v Nette mi dělá napojení jakýchkoliv
knihoven a také Composer. No, to je asi jedno, snad to jednou zvládnu,
každopádně Kdyby\Events se krásně načte, ale nějak se mi, nevím proč,
nechce načíst třída Doctrine\Common\EventManager.
Laděnka křičí:
Class Doctrine\Common\EventManager not found
File: …\libs\Kdyby\kdyby\events\src\Kdyby\Events\EventManager.php
Line: 27
26: class EventManager extends Doctrine\Common\EventManager
27: {
Nevíte, proč autoloader nenačte tuhle třídu? Potřebujete víc
informací? Jakých?
Děkuju.
Zapomněl jsem do bootstrap.php přidat odkaz na autoload.php knihovny
Kdyby\Events.
Stydím se ;-).
P.S.: stejně bych potřeboval nějaký kurz composeru a nahrávání nových knihoven do Nette :-).
Editoval svezij (14. 8. 2013 14:21)
- svezij
- Člen | 69
Mám ještě jeden dodatek, začínám se tak stydět, že bych skoro chtěl
vymazat tenhle příspěvek z databáze i z vašich pamětí.
No, došlo mi, že samozřejmě žádný prezenter nebude posluchačem.
Prezentery zpracovávají akce a spojují model a view, je tak?
„Odpálí-li“ aktér nějakou akci, prezenter o ní ví, a onu zprávu jen
vyšle do světa (resp. do event manageru). V modelu aplikace pak budou ti
posluchači (samozřejmě zaregistrovaní u event manageru) a ti dostanou
zprávu o stanivší se události a obslouží si ji, jak potřebují.
Teď už jsem to snad nespletl :-). Nj, každou minutou se učím novým a
novým věcem. Ještě jednou díky.
Editoval svezij (14. 8. 2013 15:02)
- Filip Procházka
- Moderator | 4668
@svezij: Ahoj, je to jak píšeš :) Na nějakém
objektu budeš mít událost, která bude definovaná tou property
$onSomething
, když ji pak zavoláš, tak se zpropaguje do
EventManageru a volají se posluchači, kteří jsou většinou součásít
modelu. Dobré ovšem je, že události můžeš mít i v samotném
modelu :)
Jen je tam jeden důležitý detail. Kdyby/Events
fungují pouze
na služby/objekty které jsou vytvářeny DI
Containerem. Protože se analyzují při jeho kompilaci. Události na
presenteru tedy fungovat nebudou, protože se nevytváří DI Containerem. Jde
to řešit dvěma způsoby
- Obejdeš se bez událostí na presenteru, stejně to nedává moc smysl. Daleko lepší je udělat si události v modelových třídách, jejíž metody budeš presenterem volat a v nich bude nějaká logika a volání událostí
- Pokud používáš vývojovou verzi Nette, tak je možné presentery vytvářet pomocí DI Containeru
Jinak, Kdyby/Events mají docela podrobnou dokumentaci na githubu a Composer máš vysvětlený zde
- svezij
- Člen | 69
Dobré ráno, mám nějak problém to pochopit, teoreticky to chápu, ale nějak si s tím stále nevím rady:
Obejdeš se bez událostí na presenteru, stejně to nedává moc smysl.
Daleko lepší je udělat si události v modelových třídách, jejíž metody budeš presenterem volat
a v nich bude nějaká logika a volání událostí
- co kdybych chtěl třeba logovat přístup na stránky? Nebylo by pak dobré
přímo v metodách prezenteru
action<Action>()
(neborender<View>()
) volat např. onAccess? - nebo (můj případ): v prezenteru vytvářím komponenty, že? Např.
formulář – na událost onSuccess toho formuláře navážu nějakou metodu
(např. manageUserFormSubmitted – upravuje uživatele nebo vytváří
nového), která se nachází také přímo v prezenteru. V této metodě pak
volám metodu
persist(Entity $e)
LeanMapperu pro uložení, nebo úpravu. To znamená, že musím přetížit metodupersist(Entity $e)
a v ní vyvolat mojí událostonSave
,onEdit
? Já bych navíc v tuto chvíli potřeboval pro zalogování předat nějakou zprávu o údajích, které byly změněny.
Jaksi stále nevím, jak na to :-(. Mohl byste mi, prosím, někdo vytvořit jednoduchý příklad (nemusí být nutně nijak smysluplný), kde budou popsané všechny kroky, co bych měl udělat (od zápisu do config.neon po odchycení události posluchači) který by dělal např. následující:
- Mohli byste mi to ukázat např. na příkladu, co jsem popsal výše. Po
odeslání formuláře se na onSuccess zavolá metoda pro zpracování
formuláře (neřešte žádnou logiku, jde mi jen o ty události) a v ní se
zavolá metoda
persist(Entity $e)
LeanMapperu pro uložení.
- V jedné třídě potřebuji odchytit tuto
akci
a do databáze zapsat zprávu o změně / vytvoření uživatele (Entity $e) – stačí, když zprávu jen „vyechujete“ na obrazovku. Tuto zprávu ale posluchači potřebuji „nějak“ poslat (protože tu samou akci bude vysílat každá moje entita, ale pokaždé se bude jednat o jiný objekt (uživatel, právo, dodavatel, položka objednávky, …), takže bude mít jiné položky a tudíž musí logovat i jinou zprávu - V druhé třídě zkontrolovat nějaký atribut entity a poslat například e-mail (zase stačí vypsat nějakou jinou předanou zprávu) – tzn. tady bych potřeboval na obsluhu události dostat entitu, kterou jsem změnil / vytvořil a nějaký další parametr (např. textový řetězec – to je jedno).
Bylo by to možné, prosím? Opravdu si s tím nějak nevím rady.
Snad je vše pochopitelné. Děkuji
- Filip Procházka
- Moderator | 4668
svezij napsal(a):
co kdybych chtěl třeba logovat přístup na stránky? Nebylo by pak dobré přímo v metodách prezenteru
action<Action>()
(neborender<View>()
) volat např. onAccess?
No nebylo žejo :) Protože action
method máš armádu, kdežto
událost Nette\Application\Application::onRequest
je jenom
jedna :)
nebo (můj případ): v prezenteru vytvářím komponenty, že? Např. formulář – na událost onSuccess toho formuláře navážu nějakou metodu (např. manageUserFormSubmitted – upravuje uživatele nebo vytváří nového), která se nachází také přímo v prezenteru.
Ano, asi bych měl napsat konečně ty best practices :) Událost
Form::onSuccess
je velice obecná a není dobrý nápad na ni
navazovat listenery. Volaly by se na úplně všech formulářích pro úplně
všechny listenery a musel by sis filtrovat, jestli ti přišel zrovna
formulář, který tě zajímá. Ve zkrace: nenavazuj listenery na
Form::onSuccess
;)
V této metodě pak volám metodu
persist(Entity $e)
LeanMapperu pro uložení, nebo úpravu. To znamená, že musím přetížit metodupersist(Entity $e)
a v ní vyvolat mojí událostonSave
,onEdit
? Já bych navíc v tuto chvíli potřeboval pro zalogování předat nějakou zprávu o údajích, které byly změněny.
Já LeanMapper teda nepoužívám, ale udělal bych si vlastní abstraktní
repozitář, ve kterém bych ho propojil s Kdyby\Events
.
use LeanMapper\Events;
use LeanMapper\Connection;
use LeanMapper\IMapper;
use Kdyby\Doctrine\EventManager;
abstract class Repository extends LeanMapper\Repository
{
/** @var EventManager */
private $evm;
public function __construct(Connection $connection,
IMapper $mapper, EventManager $evm)
{
parent::__construct($connection, $mapper);
$this->evm = $evm;
}
protected function initEvents()
{
static $events = array(
Events::EVENT_BEFORE_PERSIST,
Events::EVENT_BEFORE_CREATE,
Events::EVENT_BEFORE_UPDATE,
Events::EVENT_BEFORE_DELETE,
Events::EVENT_AFTER_PERSIST,
Events::EVENT_AFTER_CREATE,
Events::EVENT_AFTER_UPDATE,
Events::EVENT_AFTER_DELETE,
);
foreach ($events as $eventName) {
$ns = get_class($this);
$event = $this->evm->createEvent($ns . '::' . $eventName);
$this->events->registerCallback($eventName, $event);
}
}
}
Tohle ti vytvoří pro každý repozitář vlastní události.
namespace App;
class ArticleRepository extends Repository
{
}
services:
- App\ArticleRepository
A teď můžeš snadno udělat to, že si vytvoříš listener, který bude
naslouchat na App\ArticleRepository::beforeUpdate
a teď když
budeš updatovat entitu, zavolá se tato událost a zpraguje se do celého
systému díky Kdyby\Events
.
V jedné třídě potřebuji odchytit tuto
akci
a do databáze zapsat zprávu o změně / vytvoření uživatele (Entity $e) – stačí, když zprávu jen „vyechujete“ na obrazovku. Tuto zprávu ale posluchači potřebuji „nějak“ poslat (protože tu samou akci bude vysílat každá moje entita, ale pokaždé se bude jednat o jiný objekt (uživatel, právo, dodavatel, položka objednávky, …), takže bude mít jiné položky a tudíž musí logovat i jinou zprávu
V druhé třídě zkontrolovat nějaký atribut entity a poslat například e-mail (zase stačí vypsat nějakou jinou předanou zprávu) – tzn. tady bych potřeboval na obsluhu události dostat entitu, kterou jsem změnil / vytvořil a nějaký další parametr (např. textový řetězec – to je jedno).
Nevím jestli je tohle uplne vhodny use-case pro události. Vlastně si myslím že je to opravdu velice špatný use-case :) Normálně bych to řešil bez listenerů, čistě pomocí standardních mechanismů v Nette.
class UsersPresenter extends BasePresenter
{
/** @var ArticleRepository @inject */
public $articleRepository;
/** @var Nette\Mail\IMailer @inject */
public $mailer;
// $form->onSuccess[] = $this->userCreateClicked
public function userCreateClicked($form)
{
// nevim jak se pouziva LM a nechci to hledat :)
$user = $this->articleRepository->persist($form->values);
$message = new Nette\Mail\Message;
// nastaveni emailu
$this->mailer->send($message);
$this->flashMessage("Uživatel byl vytvořen");
$this->redirect('editProfile', $user->id);
}
}
namespace App;
class ArticleRepository extends \App\Repository
{
}