Observer v Nette – mám správný návrh?

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

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:

  1. logovat akce uživatele – odpovídat na události vyvolané prezentery (zde se jedná o observer)
  2. 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
+
0
-

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
+
0
-
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:

  1. zalogovat, že uživatel změnil stav objednávky
  2. jedním posluchačem odchytit tuhle událost a odeslat e-mail zákazníkovi
  3. druhým posluchačem odchytit tuhle událost a změnit skladovou dostupnost
  4. 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
+
0
-

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
+
0
-

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
+
0
-

@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
+
0
-

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>() (nebo render<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 metodu persist(Entity $e) a v ní vyvolat mojí událost onSave, 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í.
  1. 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
  2. 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
+
+1
-

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>() (nebo render<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 metodu persist(Entity $e) a v ní vyvolat mojí událost onSave, 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
{

}
svezij
Člen | 69
+
0
-

Nevím proč, ale tohle je asi to nejlepší fórum, co jsem kdy potkal – fakt skvělá práce všichni :-), mockrát děkuji za pomoc, teď už si snad poradím :-)… a snad… jednoho dne budu i já někomu takhle nápomocný ;-)