Event DI Extension feature
- Lukes
- Silver Partner | 68
Zdravím všechny,
v době, kdy jsem přecházel na Nette 3, jsem hledal náhradu za Kdyby/Events, bez nutnosti instalovat půlku Symfony a k tomu nainstalovat knihovny, které počítají trajektorii na Mars a následně spustí jeho teraformaci. Ti co používají NPM vědí o čem mluvím.
Přemýšlím, že bych se podělil o své řešení, které by bylo fajn přímo v Nette pro všechny. Proto od vás chci vědět, jestli se taková funkce bude líbit a jestli mám připravit pull request do Nette. Zbytečně se mi to dělat nechce.
Moje motivace je použít pouze nativní součásti Nette:
- DI Extension
- Eventy nad
SmartObject
Dost často je potřeba udělat nějakou akci (poslat SMS, email…), když se stane jiná (registrace, změna hesla…) a předávat do registrace mailer a SMS sender je poměrně nepraktické, protože pak něco takového testovat je problém.
<?php declare(strict_types=1);
class Registration
{
use Nette\SmartObject;
/** @var callable[] */
public array $onRegister = [];
public function register(... $data): void
{
/** ... */
$this->onRegister($data);
{
}
To dost často spousta lidí napojí někde v presenteru na nějakou closure nebo to předeleguje jinam, což asi úplně blbě není, ale dost to znepřehlední Presenter/Component a stejně ty závislosti musím někde vzít.
Na toto se hodí návrhový vzor Producer/Consumer. Producer už máme výše a chtěli bychom vytvořit i consumer.
<?php declare(strict_types=1);
class ProjectMailer
{
use Nette\SmartObject;
private Nette\Mail\Mailer $mailer;
public function __construct(Nette\Mail\Mailer $mailer)
{
$this->mailer = $mailer;
}
public function sendRegistrationMail(array $data): void
{
$message = new Nette\Mail\Message(...$data);
$this->mailer->send(message);
}
public function sendSupportMail(string $email, string $name, array $data): void
{
$message = new Nette\Mail\Message(...$data);
$this->mailer->send(message);
}
}
Tento kód je dobře samostatně testovatelný, čitelný a mohu na jednu událost do šířky připojovat další akce (poslat SMS, zalogovat, spočítat statistiky…), které mohou mít vlastní třídu přímo s jednou odpovědností.
Jak to propojit?
Myslím, že by se o to mohl v klidu postarat DI kontejner.
events:
listeners:
project_mailer:
create: ProjectMailer
events:
Registration::onRegister: sendRegistrationMail
SupportForm::onConfirm: sendSupportMail
Takto vypadá konfigurace DI extension, která by vše spojila pouze na
vygenerovaném DI kontejneru. Vše je samozřejmě lazy. Eventy do
Registration
se přidají až v době, kdy se
Registration
vytváří a ProjectMailer
se vytváří
až v momentě zavoláním eventu.
- Co si to tom myslíte? – @DavidGrudl
- Má smysl tuto funkci přidat nativně do Nette?
- Mám připravit pull request?
- V jakém balíčku (DI, Application, Utils…) by tato funkce měla být?
Editoval Lukes (4. 3. 2020 19:20)
- David Grudl
- Nette Core | 8227
Jestli to dobře chápu, cílem je prohodit místo, kde se nastavují události, mezi producerem a consumerem?
services:
project_mailer: ProjectMailer
registration:
create: Registration
setup:
- '$project_mailer[]' = [@project_mailer, sendRegistrationMail]
Změnit ně něco jako:
services:
registration: Registration
project_mailer:
create: ProjectMailer
events:
@registration::onRegister: sendRegistrationMail
- Lukes
- Silver Partner | 68
Díky za reakci.
Musím se přiznat, že na tuhle syntax bych asi nepřišel a nikde sem si nevšiml, že by to bylo v dokumentaci, což netvrdím, že to tam není ;)
- '$project_mailer[]' = [@project_mailer, sendRegistrationMail]
Ono to vlastně udělá skoro to stejné. Do createServiceXXX()
metody na DI kontejneru to udělá přiřazení callbacků. Co mi vadí na tvém
řešení je to, že vlastně toho consumera vytvoří při vytvoření
producera, protože nageneruje následující kód.
$service->onRegister[] = [$this->getService('project_mailer'), 'sendRegistrationMail'];
To okamžitě vyvolá metodu getService
, což může vytvořit
docela velký strom služeb, které z 99% použití nemusí být potřeba.
Osobně bych viděl lepší vygenerovat něco jako:
$service->onRegister[] = function(...$parameters)
{
$this->getService('project_mailer')->sendRegistrationMail(...$parameters);
};
Tvůj návrh se mi líbí:
services:
registration: Registration
project_mailer:
create: ProjectMailer
events:
@registration::onRegister: sendRegistrationMail
Spíš než prohodit tu definici mi jde o to, mít možnost nějak lépe definovat ty eventy. Můžeš to udělat i obráceně:
services:
registration:
create: Registration
events:
onRegister: [@project_mailer, sendRegistrationMail]
# nebo
onRegister: @project_mailer::sendRegistrationMail
project_mailer: ProjectMailer
Pak už je to možná o diskuzi, co je vhodnější.
Edit: Když nad tím přemýšlím, přiklonil bych se k variantě definice eventů u consumera. Protože pokud chci oddělit business logiku od tech přidružených akcí, tak mi to přijde logičtější. Co myslíte?
Editoval Lukes (5. 3. 2020 9:46)
- MarcelSup
- Člen | 2
<?php
namespace App\DI;
use Nette\DI\CompilerExtension;
class EventsExtension extends CompilerExtension
{
public function loadConfiguration()
{
$builder = $this->getContainerBuilder();
$config = $this->getConfig();
foreach ($config['listeners'] as $listenerName => $listenerConfig) {
$listenerService = $builder->addDefinition($listenerName)
->setFactory($listenerConfig['create']);
foreach ($listenerConfig['events'] as $event => $method) {
[$producer, $producerEvent] = explode('::', $event);
$producerService = $builder->getDefinitionByType($producer);
$producerService->addSetup('$' . $producerEvent . '[]', [
[$listenerService, $method]
]);
}
}
}
}
> extensions:
events: App\DI\EventsExtension
events:
listeners:
project_mailer:
create: ProjectMailer
events:
Registration::onRegister: sendRegistrationMail
SupportForm::onConfirm: sendSupportMail
>
>
- MarcelSup
- Člen | 2
nebo přímo takhle :
<?php
namespace Nette\DI;
use Nette\DI\CompilerExtension;
class EventsExtension extends CompilerExtension
{
public function loadConfiguration()
{
$builder = $this->getContainerBuilder();
$config = $this->getConfig();
foreach ($config['listeners'] as $listenerName => $listenerConfig) {
$listenerService = $builder->addDefinition($listenerName)
->setFactory($listenerConfig['create']);
foreach ($listenerConfig['events'] as $event => $method) {
[$producer, $producerEvent] = explode('::', $event);
$producerService = $builder->getDefinitionByType($producer);
$producerService->addSetup('$' . $producerEvent . '[]', [
[$listenerService, $method]
]);
}
}
}
}
Aktualizujte svůj konfigurační soubor config.neon tak, aby zahrnoval nové rozšíření:
yaml
extensions:
events: Nette\DI\EventsExtension
events:
listeners:
project_mailer:
create: ProjectMailer
events:
Registration::onRegister: sendRegistrationMail
SupportForm::onConfirm: sendSupportMail