Šablona emailu z databáze
- Aishak
- Člen | 30
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
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
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
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
Je potřeba vytvářet engine přes Nette\Bridges\ApplicationLatte\LatteFactory, pak je cache vyřešena
- Polki
- Člen | 553
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
@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
@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
a jeden chytrý člověk taky řekl toto :)
premature optimization is the root of all evil
- Polki
- Člen | 553
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.