Event DI Extension feature

Lukes
Silver Partner | 68
+
+3
-

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

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

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)

Kori
Člen | 73
+
0
-

Taky bych se priklanel definovat eventy u consumera.

MarcelSup
Člen | 2
+
0
-
<?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
+
0
-

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