Šablona emailu z databáze

Aishak
Člen | 30
+
0
-

Zdravím,

přebírám projekt a potřeboval bych fixnout jako první odesílání emailů. Aktuálně je to vytvořený tak, že je soubor mail/layout.latte, který slouží jako rodič pro všechny typy emailů. V tom samozřejmě není problém. Funguje správně.

Nicméně, všechny typy emailů jsou v DB jako text (sloupec ‚content‘). Text se následně předá jako proměnná do šablony mail/layout.latte, ale v emailu z DB jsou další proměnné, které je třeba plnit. To se už neděje, takže na výstupu je klasicky text třeba:

Dobrý den, {$firstname}

místo toho, aby tam bylo:

Dobrý den, Tomáši

Proměnné v šabloně jsou. Noescape filtr jsem zkoušel. Nevěděl by někdo jak na to prosím?
Děkuji

Nette 2.4, PHP 7.4

Editoval Aishak (30. 7. 2021 23:19)

Polki
Člen | 553
+
0
-

Tohle neuděláš.
Musela šablona by pak nešla jednoduše vykreslit, protože nad výstupem by se znovu volalo parsování šablony, takže by se šablona musela ukládat někam do paměti a tedy by to nebyl čistý PHP kód volající echo, ale nějaké parsování a ukládání do řetězce, nad výsledkem průchod, jestli se náhodou ve výsledku nevyskytuje proměnná a potom znova parsování, znova vyhledávání, znova parsování.. (Protože ty další proměnné v sobě můžou mít další proměnné…)

To by neskutečně vykreslování šablon zpomalilo a ještě by to ve většině případů bylo zbytečné.

Řešení pro tebe je vzít mail/layout.latte předat do něj proměnnou z databáze s E-mailem, vykreslit to do řetězce pomocí $engine->renderToString() a výsledek, tedy novou šablonu uložit na disk (třeba do temp složky) a znovu jí předávat proměnné a ukládat do řetězce.

Reálně by to mohlo vypadat nějak takto:

$engineLayout = new \Latte\Engine();			// vytvoříme latte engine pro layout mailů
$emailLattePath = TEMP_DIR . '/emails/' . $nameOfTheEmail . '.latte';	// vygenerujeme si temp cestu k vygenerované šabloně

\Nette\Utils\FileSystem::write($emailLattePath, $engineLayout->renderToString(__DIR__ . '/mail/layout.latte', $emailFromDb)); // do temp souboru šablony uložíme layout s obsahem E-mailu z db. Tento bude obsahovat to, co píšeš, tedy **Dobrý den, {$firstname}**

$engineEmail = new \Latte\Engine();	// vytvoříme si druhý engine, kterým budeme parsovat obsah temp šablony
$engineEmail->setSandboxMode(true); // Nesmíme zapomenout nastavit sandbox mód, jelikož toto je šablona, která je generovaná, tak nechceme, aby ten, co ji generuje měl přístup k proměnným mimo ten E-mail

$message = new Nette\Mail\Message(); // klasicky už vytvoříme message
...
$message->setHtmlBody($engineEmail->renderToString($emailLattePath, $otherDataToEmailBody)); // vykreslíme naši temp šablonu s daty, která do ní patří *['firstname' => 'Tomáši']*
...
$sender = new Nette\Mail\SendmailMailer();
$sender->send($message); // odešleme mail, který již v sobě bude mít *Dobrý den, Tomáši*

Na závěr chci říct, že toto je náčrt. Nemělo by to vypadat přesně takto. Použij továrničky, nějak to učesej. C+V není ideální. Jo a možná by bylo dobré pokud tělo mailů vypadá pořád stejně negenerovat tu temp šablonu pořád znova, ale ještě checkovat, jestli už šablona daného názvu prostě neexistuje.

Aishak
Člen | 30
+
0
-

Díky moc. Mrknu na to :-)

Aishak
Člen | 30
+
0
-

nightfish napsal(a):

@Aishak Mrkni na https://latte.nette.org/cs/develop#…

To vypadá přesně na to, co potřebuji. Díky moc!

Polki
Člen | 553
+
-1
-

Aishak napsal(a):

nightfish napsal(a):

@Aishak Mrkni na https://latte.nette.org/cs/develop#…

To vypadá přesně na to, co potřebuji. Díky moc!

Stejně bych ale, pokud se ti s každým dotazem nemění obsah textace mailu, zvážil nějaké cachování mezivýsledku jak jsem psal. Ať už do souboru, nebo do cache od Nette apod. a to proto, že asi nechceš překládat ten stejný soubor pořád dokola při každém odeslání mailu.
To, co psal nightfish je super věc a dá se zkombinovat třeba s cachí.

David Matějka
Moderator | 6445
+
+3
-

Je potřeba vytvářet engine přes Nette\Bridges\ApplicationLatte\LatteFactory, pak je cache vyřešena

Polki
Člen | 553
+
0
-

David Matějka napsal(a):

Je potřeba vytvářet engine přes Nette\Bridges\ApplicationLatte\LatteFactory, pak je cache vyřešena

@DavidMatějka takže říkáš, že když použiji zápis takto:

public function __construct(
	private \Nette\Bridges\ApplicationLatte\LatteFactory $latteFactory,
) { }

public function sendMail(): void
{
	$mailLatteContent = 'Ahoj {$name}'; // to 'Ahoj {$name}' je načteno z DB
	$mailData = [
		'name' => 'Honzo',
	];									// Celé toto pole načteno z db

	$latte1 = $this->latteFactory->create();

	$generatedMailLatte = $latte1->renderToString('/path/to/mail.latte', [  // obsah šablony je cca '<!DOCTYPE html><html><head></head><body>{$content|noescape}<br>S pozdravem,<br>Tým teamname</body></html>'
		'content' => $mailLatteContent,
	]);

	$latte2 = $this->latteFactory->create();

	$latte2->setLoader(new \Latte\Loaders\StringLoader([
		'mail.file' => $generatedMailLatte,
	]));

	$mailHtmlContent = $latte2->renderToString('mail.file', $mailData);

	// Send email...
}

tak se obsah proměnné $generatedMailLatte zacachuje a tedy se nebude při odeslání mailů obsah té proměnné generovat znovu a znovu?
Chápu, že si latte cachuje šablony, které má vykreslovat, takže si zacachuje obsah ‚/path/to/mail.latte‘ a potom v StringLoaderu zacachuje obsah proměnné $generatedMailLatte jako obsah nové šablony přetransformovaný do nativního PHP…

Ale myslím si, že při volání $latte1->renderToString(‚/path/to/mail.latte‘, …); se výsledek nenačítá z cache podle parametrů, ale vezme se zkompilovaný latte soubor ‚/path/to/mail.latte‘ a do něj se pošle proměnná $content a tedy se provede ‚vykreslení‘, což znamená konkatenaci 3 řetězců, včetně aplikace filtrů na proměnnou content, což se sice provádí ryhle, ale pokud funkci sendMail budu volat 10000× kvůli například rozesílání notifikací, tak se na mě nezlob, ale pro tento konkrétní mail by byla šablona vždy stejná a tak mi přijde logické si obsah proměnné $generatedMailLatte uložit někam do cache a volat nové vykreslení jen, když se změní obsah proměnné $mailLatteContent

David Matějka
Moderator | 6445
+
+1
-

@Polki mas to tam spatne, nejdriv musis vyrenderovat mailLatteContent spolu s mailData pomoci StringLoaderu (a dost mozna se zapnutym sandboxem) a potom ten obsah poslat jako content k renderovani mail.latte layoutu. A pak tam nemas co cachovat, prvni krok cachovat nejde, jelikoz je dynamicky a druhy krok tim padem taky ne, jelikoz zavisi na vygenerovane sablone. A co se rychlosti tyce, tak 10000× vyrenderovat tu sablonu je zalezitost pod 1 sekundu.

btw, jde to udelat i pres jedine volani renderToString, se StringLoaderem zhruba takto:

	$latte->setLoader(new \Latte\Loaders\StringLoader([
		'main.latte' => '<!DOCTYPE html><html><head></head><body>{include content.latte}<br>S pozdravem,<br>Tým teamname</body></html>',
		'content.latte' => 'Hello, {$name}',
	]));
	$html = $latte->renderToString('main.latte', ['name' => 'John']);

pripadne s nejakym custom loaderem, ktery dovoli kombinovat soubory na disku a string sablony.

Polki
Člen | 553
+
0
-

@DavidMatějka To, co popisuju já mi přijde jako:
vytvoř vozík o velikosti takové, aby se ti do něj vlezla krabice A, do které můžeme dávat náklad daného typu a tento vozík poté používej na převoz všech nově vytvořených krabic typu A nehledě na jejich content (zacachujeme vozík)

To, co píšeš ty mi přijde jako:
vezmi content daného typu, na něj vytvoř krabici A a pro tuto krabici A vytvoř vozík, ve kterém budeme tu krabici převážet. (vozík se tvoří pořád dokola)

řešení, co jsi napsal teď naposled se mi líbí, ovšem pokud chci mít šablony prostě v souboru, tak bych tu základní musel načítat ze souboru, nebo jak píšeš udělat si vlastní loader a navíc to neřeší to, co píšu a to, že by bylo dobré si vozík (tj. <!DOCTYPE html><html><head></head><body>Hello, {$name}<br>S pozdravem,<br>Tým teamname</body></html>) uložit do cache. (nebo řeší?)

A to, že to trvá 1s pro mě není úplně argument. Mailů může být více, méně, atd. a když si to takto řeknu všude, tak pak vidím, jak se aplikace místo 30ms načítá 2 vteřiny.
Jednou jeden chytrý člověk řekl, že pokud implementuješ novou fičuru a ta nová fičura zpomalí vykonávání daného kódu třeba 2×, tak je to vždycky špatně a je jedno, jestli jsi aplikaci zpomalil z 2 vteřin na 4, nebo z 1ms na 2ms. Chyba je to vždy. Aplikaci můžeš zpomalit vždy jen o nejnižší nutný počet k tomu, aby vše fungovalo, ale zbytečně nežralo resources.
No a myslím, že vykreslovat 1 šablonu, nebo 2 šablony je zpomalení 2× (záleží sice na obsahu obou šablon, počtu proměnných apod. ale pořád je to větší zpomalení, než jaké je nutné.)

EDIT 1:
Chápu, že ten řetězec se uloží do cache přeložený do nativního PHP. Mě jde o to, aby se negeneroval pořád znovu ten řetězec.

EDIT 2:
Koukám do cache. Vidím, že ten tvůj poslední zápis vygeneruje 2 zacachované soubory, kde jeden načítáhned ten druhý, takže je tam sice nějaký load navíc, ale už se znova negeneruje ten řetězec nové šablony, takže za mě poslendní řešení je BOŽÍ

Editoval Polki (2. 8. 2021 12:33)

David Matějka
Moderator | 6445
+
+1
-

a jeden chytrý člověk taky řekl toto :)

premature optimization is the root of all evil

Polki
Člen | 553
+
0
-

Tak ten asi moc chytrý nebyl, jelikož máme zkušenosti s tím, že když se vše píše od začátku čistě a dobře, tak výsledná aplikace běží dobře a rychle a není třeba do ní hrábnout, zatímco, když děláme optimalizace až zpětně, tak nad samotnou prací strávíme 70–90% více času, než když u psaní nad kódem přemýšlíme dopředu a to v optimálních případech, kdy kvůli optimalizaci není třeba půl kódu přepisovat.

Prostě když vidíš, že něco má složitost O(N) a hned víš, že jde to udělat tak, aby byla složitost O(k), tak proč by si psal kód špatně a doufal, že to nebude s tou rychlostí tak zlé, když víš, jak to rovnou napsat tak, aby si měl jistotu, že to lépe nejde a tím pádem ušetříš tuny času.

Jsou místa, kdy je optimalizace na škodu, protože je její implementace hodně náročná, nebo samotná implementace optimalizace sice zrychlí kód, ale optimalizační kód aplikaci zpomalí. (jako kdyby jsi měl třídu A, jejíž metoda run trvá vykonat 3 vteřiny, tak si napíšeš optimalizační metodu runOptimized, která obaluje tu metodu run a to tak, že nastaví třídu A tak, že metoda run poběží 1 vteřinu, ale samotné nastavování v metodě runOptimized zabere 2 vteřiny…) Pak je optimalizace zbytečná a k ničemu. Dobrý programátor ale tyto místa a zbytečné optimalizace odhalí ještě dřív, než píše kód…

Na internetu najdeš spoustu stránek, kde se na tuto problematiku zaměřují přímo úkoly. Jsou to stránky, kde máš zadání, naprogramuješ výstup, nahraješ kód do formu a hned vidíš výsledek jak sis vedl. Jsou tam úkoly typu prohledávání v poli, průchod vícerozměrným polem se součtem apod., kde dummy řešení může trvat i několik hodin a následná optimalizace znamená překopat celý kód, protože je třeba přepsat podstatu procházení, nebo indexaci, generační algoritmus apod., takže následná optimalizace znamená, že strávíš tunu času nad kódem, který pak stejně zahodíš a musíš psát nový lepší…
V takových případech je na soutěžích, kde se tyto problémy řeší zdokumentováno, že pokud se to řeší dopřednou optimalizací a tedy že si sedneš a popřemýšlíš po problému, tak výsledek je možné naprogramovat za 20 minut, ale pokud to děláš tak, že píšeš dummy řešení, které pak vylepšuješ, tak je vysoká pravděpodobnost, že nestihneš odevzdat odladěný výsledek do maximálního času, který činí 5 hodin.