Silex: kontaktní formulář a posílání e-mailů

Po měsíční odmlce budeme pokračovat v seriálu o tvorbě webu s využitím mikroframeworku Silex. Podíváme se, jak vytvořit formulář podobným způsobem jako v Symfony 2 a jak data z něj následně odesílat na e-mailovou adresu. Jinými slovy, do našeho ukázkového webu doplníme kontaktní formulář.

Reklama

Jedna z podstránek vzorového webu, který stavíme na Silexu s využitím šablony BisLite, obsahuje kontaktní formulář. Zkusíme jeho původní HTML podobu zahodit a vykreslit jej ve Twigu pomocí postupu, kterým se pracuje s formuláři i ve větším frameworku Symfony 2, ze kterého Silex vychází.

Formuláře v Silexu

Začneme tím, že do našeho projektu přidáme knihovny pro práci s formuláři. Opět sáhneme po nástroji Composer a do příkazového řádku ve svém projektu napíšeme:

composer require symfony/form symfony/security-csrf symfony/twig-bridge symfony/translation

Composer stáhne a do projektu přidá jak podporu formulářů jako takových, tak zabezpečení proti CSRF útokům a podporu vkládání formulářů do šablon v Twigu.

Na začátek souboru index.php je nyní třeba vložit informaci o tom, že budeme používat komponenty pro práci s formuláři. Spolu s tím přidáme podporu vyhodnocování požadavků HTTP, abychom mohli reagovat na odeslání formuláře.

use Silex\Provider\FormServiceProvider;
use Symfony\Component\HttpFoundation\Request;

Za definici Silex\Application ještě přidejte registraci poskytovatele formulářů a tím je implementace jejich podpory v Silexu hotova.

$app->register(new FormServiceProvider());
$app->register(new Silex\Provider\TranslationServiceProvider(), array(
    'translator.domains' => array(),
));

Sestavení kontaktního formuláře

Dalším krokem nyní bude sestavení kontaktního formuláře a předání informací o něm do šablony v Twigu. Najděte si definici stránky na adrese kontakt. Z minulých dílů máme tuto routu zapsánu následovně:

$app->get('/kontakt', function() use($app) {
  return $app['twig']->render('kontakt.html.twig');
})->bind('kontakt');

Tato definice postačuje pro případ, že načítáme adresu /kontakt, tedy tento požadavek se objevuje v URL adrese. Nyní nám ale stačit nebude. Potřebujeme, aby Silex reagoval také na to, že na tuto adresu odešleme formulář metodou POST, tedy, že jeho data nebudou v URL vidět. Tak jako má GET a POST zpracování formulářů v HTML, podobně může reagovat i Silex pomocí $app->get() a $app->post().

Pokud chcete poslouchání na dané adrese spojit pro obě metody dohromady, musíme nahradit původní $app->get() za funkci $app->match(). Jako argument anonymní funkce, která je v definici uvedena a slouží k sestavení dat pro Twig, přidáme ještě Request $request. To proto, že v requestu budou umístěna data z odeslaného formuláře, ke kterým budeme přistupovat.

Čili definice funkce poslouchající na adrese /kontakt bude mít nově následující kostru:

$app->match('kontakt', function (Request $request) use ($app) {...})->bind('kontakt');

V těle této kostry se pustíme do sestavení formuláře. V základní podobě to není nic složitého a jakmile se to naučíte, využijete stejný postup i ve velkém frameworku Symfony 2.

Sestavený formulář vložíme do proměnné $form. Sestavíme jej tím, že v naší aplikaci zavoláme tag form.factory a jeho funkci createBuilder(‘form’). Ke vzniklému objektu pak jen pomocí funkcí add() přidáme požadovaná políčka formuláře.

Funkce add() očekává několik argumentů, z nichž povinný je pouze ten první, ve kterém specifikujete jméno formulářového políčka. Neuvedete-li nic dalšího, pak se prostě vykreslí základní textové pole formuláře s patřičným atributem name. Příklad:

->add('telefon')

Můj příklad, ke kterému za chvíli dojdete, je o něco složitější. Přidáním druhého argumentu do funkce add() totiž sami určíte, jaký formulářový prvek chcete vykreslit. Místo textového pole to může být například textarea nebo prvek z HTML5 označený jako email (čili textové pole validující zadání e-mailové adresy). Příklad:

->add('vzkaz', 'textarea')

Já v příkladu používám ještě třetí argument, kterým je pole obsahující informace pro nastavení daného formulářového prvku. V příklad můžete vidět, jak by se k prvkům ve formuláři přidala třída CSS, případně jak by se u víceřádkového textového pole upřesnil počet řádků, se kterými se má vykreslit.

Kód pro sestavení formuláře se jménem, e-mailovou adresou, předmětem a tělem vzkazu bude následující:

$form = $app['form.factory']->createBuilder('form')
    ->add('name', 'text', array('attr'  =>  array('class'   => 'form-control') ))
    ->add('email', 'email', array('attr'  =>  array('class'   => 'form-control') ))
    ->add('subject', 'text', array('attr'  =>  array('class'   => 'form-control') ))
    ->add('message', 'textarea', array('attr'  =>  array('class'   => 'form-control', 'rows' => 10,) ))
    ->getForm();

Tím je formulář sestaven a zbývá funkci zakončit tím, že už známým způsobem zavoláme vykreslení obsahu stránky pomocí šablony Twig. Jen jí předáme informaci o sestaveném formuláři:

return $app['twig']->render('kontakt.html.twig', array(
    'form' => $form->createView(),
));

Do šablony kontakt.html.twig jako takové je samozřejmě třeba přidat vykreslení sestaveného formuláře:

{{ form_start(form) }}
{{ form_errors(form) }}
{{ form_rest(form) }}
<input type="submit" value="Odeslat" size="" maxlength="" />
{{ form_end(form) }}

První zápis vytvoří ve výstupním HTML značku <form> se všemi náležitostmi, další zobrazí případné chyby validace po odeslání formuláře. Zápis form_rest() slouží k tomu, aby se vykreslil zbytek definicí ve formuláři, které se ještě nevykreslily (tj. políčka a jejich popisky). Ručně přidáme tlačítko pro odeslání formuláře a zápis pro jeho ukončení. Všimněte si, že ve funkcích je jako argument uvedeno form, tedy název proměnné, kterou v index.php předáváme do Twigu.

Poznámka: Díky tomu, že jsme do projektu zahrnuli ochranu před CSRF, tak Silex do formulářů sestavených tímto způsobem automaticky přidá i skryté políčko s tokenem, který následně vyhodnocuje. Automaticky tak máte formulář chráněn před útokem.

Zpracování dat z formuláře a jeho odeslání na e-mail

Formulář by se vám měl nyní zobrazovat, automaticky kontroluje, že jsou všechna políčka vyplněna a rovněž ověřuje, zda jste do e-mailu vyplnili e-mailovou adresu. Když formulář odešlete, nic se nestane, protože nemáme obsluhu zpracovávající data z něj. Což ale napravíme.

Budeme používat knihovnu SwiftMailer, kterou je do projektu potřeba přidat opět pomocí Composeru, tj. příkazem:

composer require swiftmailer/swiftmailer

Někde za definici proměnné $app v souboru index.php přidejte registraci poštovní knihovny:

$app->register(new Silex\Provider\SwiftmailerServiceProvider());

Opět se vrátíme do funkce definující routu pro adresu /kontakt. Za definici formuláře do proměnné $form přidejte podmínku:

if ('POST' == $request->getMethod()){...}

V ní budeme formulář zpracovávat, takže s dalšími kroky zůstáváme v ní. Protože přes $app->match() odchytáváme jak načtení stránky (metoda GET), tak odeslání formuláře z ní (metoda POST), touto podmínkou jednoduše vyhodnotíme, že je volána právě metoda POST.

Pomocí funkce bind() propojíme výše definovaný formulář s tím, co se nám vrátilo při zpracování jako požadavek na stránku a následně si do proměnné $data uložíme údaje z vyplněného formuláře.

$form->bind($request);
$data = $form->getData();

Použijeme další podmínku, kterou ověříme, zda jsou data z formuláře validní. Nemůžeme totiž spoléhat jen na ověřování v HTML5 na straně prohlížeče.

if ($form->isValid()) {...}

Do této podmínky pak doplníme úkoly, které chceme provést poté, co jsme se ujistili, že jsme obdrželi validně vyplněný formulář. V našem případě to bude sestavení mailové zprávy a její odeslání.

Začneme trochu bokem. Mezi ostatní šablony Twigu, tedy do složky views si přidejte nový soubor mail.html.twig. V tomto souboru definujeme tělo odesílaného e-mailu. Vypadat může nějak podobně:

Zákazník: {{data.name}}
E-mail: {{data.email}}

---Zpráva---
 
{{data.message}}

Jak vidíte, do těla e-mailu musíme předat proměnnou data, která bude obsahovat položky name, email a message, tedy položky z vyplněného formuláře. Tuto proměnnou data jsme si již připravili v souboru index.php, kam se vrátíme zpět a sestavíme tělo zprávy předáním proměnné $data do šablony:

$body = $app['twig']->render('mail.html.twig', array('data' => $data));

Nyní sestavíme mailer, který zprávu odešle:

$app['mailer'] = $app->share(function ($app) {
    return new \Swift_Mailer($app['swiftmailer.transport']);
});

Připravíme také samotnou zprávu skládající se z nové instance Swift_Message, které zároveň definujeme předmět zprávy, odesilatele, adresáta a tělo zprávy.

$message = \Swift_Message::newInstance()
    ->setSubject($data['subject'])
    ->setFrom($data['email'])
    ->setTo('adresat@domena.tld')
    ->setBody($body);

Nyní už zbývá zprávu odeslat, což je jednoduchý příkaz:

$app['mailer']->send($message);

Po odeslání zprávy bychom měli znovu načíst stránku kontakt, ale zároveň bychom měli návštěvníkovi stránky sdělit nějaké upozornění, že zpráva byla odeslána. V Symfony 2 a Silexu k tomu můžete využít tzv. FlashBag, do kterého přidáme nějakou jednorázově zobrazenou informaci a tu vykreslíme v Twigu.

Někam za definici $app doplňte registraci provideru pro práci se sessions:

$app->register(new Silex\Provider\SessionServiceProvider());

Předání informace do FlashBagu a následné znovu načtení stránky s kontaktním formulářem provedete takto:

$app['session']->getFlashBag()->add('notice','Zpráva byla odeslána');
return $app->redirect('kontakt');

V šabloně kontakt.html.twig ještě přidejte na místo, kde chcete zprávu zobrazit, tento zápis:

{% for flashMessage in app.session.flashbag.get('notice') %}
<div class="notice">
  {{ flashMessage }}
</div>
{% endfor %}

Kompletní zápis definice routy na adresu /kontakt, která sestaví a zpracuje formulář, je následující:

$app->match('kontakt', function (Request $request) use ($app) {
    $form = $app['form.factory']->createBuilder('form')
        ->add('name', 'text', array('attr'  =>  array('class'   => 'form-control') ))
        ->add('email', 'email', array('attr'  =>  array('class'   => 'form-control') ))
        ->add('subject', 'text', array('attr'  =>  array('class'   => 'form-control') ))
        ->add('message', 'textarea', array('attr'  =>  array('class'   => 'form-control', 'rows' => 10,) ))
        ->getForm();
    if ('POST' == $request->getMethod()) {
        $form->bind($request);
        $data = $form->getData();
        if ($form->isValid()) {
            $body = $app['twig']->render('mail.html.twig', array('data' => $data));
            $app['mailer'] = $app->share(function ($app) {
                return new \Swift_Mailer($app['swiftmailer.transport']);
            });
            $message = \Swift_Message::newInstance()
                ->setSubject($data['subject'])
                ->setFrom($data['email'])
                ->setTo('jan.polzer@gmail.com')
                ->setBody($body);
            $app['mailer']->send($message);
            $app['session']->getFlashBag()->add('notice','Zpráva byla odeslána');
            return $app->redirect('kontakt');
        }
    }
    return  $app['twig']->render('kontakt.html.twig', array(
        'form' => $form->createView(),
    ));
})->bind('kontakt');

Úprava vzhledu formuláře

Použili jsme sice šablonu, ale tím, že se nám formulář vykreslil v nějaké výchozí neupravené podobě HTML, tak je ošklivý a neodpovídá tomu, jak vypadal výchozí formulář v šabloně. Porovnejte na obrázku. Vlevo je náš formulář, vpravo pak ten ukázkový z HTML šablony.

Nový a původní formulář ze šablony

Podíváte-li se na původní formulář v šabloně, pak vidíte, že má jiné ID. Rovněž by nebylo na škodu mít popisky formuláře česky. Jak na to? Musíme poněkud upravit způsob, jakým se formulář generuje v šabloně kontakt.html.twig.

Bude to ale stát trochu práce navíc, protože nyní budeme jednotlivá políčka formuláře, která jsou v něm definována vykreslovat ručně. Začátek je podobný, tedy otevřeme formulář. Všimněte si ale doplněného zápisu pro uvedení atributů. Přidal jsem ID formuláře, které je stejné, jako měl původní HTML formulář v šabloně.

Následně do doplněné HTML značky fieldset vykresluji postupně jednotlivá políčka, tentokrát s doplněním atributů pro šířku, a jejich popisky. Formuláře v Silexu a Symfony 2 jsou natolik chytré, že poté, co takto vykreslím jednotlivá políčka, tak funkce form_rest() je už znovu nezopakuje a jen dokreslí, co jsem ručně nespecifikoval, v tomto případě skryté políčko s kontrolním tokenem.

Můžete vyzkoušet, že formulář pak bude vypadat stejně, jako ten původní ze šablony BisLite. Jeho nový kód je následující:

{{ form_start(form, {'attr': {'id': 'contact_form'}}) }}
{{ form_errors(form) }}
  <fieldset>
    <ol>
      <li>
         <label >{{ form_label(form.name,'Jméno') }}</label>
         {{ form_widget(form.name, {'attr': {'size': '77'}}) }}
      </li>
      <li>
         <label >{{ form_label(form.email,'E-mail') }}</label>
         {{ form_widget(form.email, {'attr': {'size': '77'}}) }}
      </li>
      <li>
         <label>{{ form_label(form.subject,'Předmět') }}</label>
         {{ form_widget(form.subject, {'attr': {'size': '77'}}) }}
      </li>
      <li>
         <label >{{ form_label(form.message,'Zpráva') }}</label>
         {{ form_widget(form.message, {'attr': {'cols': '57', 'rows': '15'}}) }}
      </li>
    </ol>
  </fieldset>
{{ form_rest(form) }}
<input type="submit" value="Odeslat" size="" maxlength="" />
{{ form_end(form) }}

Úkol pro vás

Řešení uvedené v tomto článku spoléhá na to, že zpráva bude na e-mail odeslána lokálním poštovním serverem. To zřejmě nebude problém na hostingu. Pokud budete příklad zkoušet na lokálním počítači, zkuste využít prográmek smtp4dev.

SwiftMailer samozřejmě můžete přesvědčit i k tomu, aby použil externí SMTP server. V tom případě doplňte za definici $app[‘mailer’] toto nastavení:

$app['swiftmailer.options'] = array(
  'host' => 'mailtrap.io',
  'port' => '2525',
  'username' => 'nejakejmeno',
  'password' => 'nejakeheslo',
  'encryption' => null,
  'auth_mode' => 'cram-md5'
);

Poznámka: Výše uvedený kód využívá externí SMTP server služby mailtrap.io sloužící k testování odesílaných e-mailů, které ale ve skutečnosti neodešle, drží je u sebe a umožňuje vám je prohlížet. Vřele doporučuji vyzkoušet alespoň její bezplatnou variantu.

Tip: Externí SMTP server využijete také k tomu, abyste se ujistili, že vaše zprávy opravdu k adresátovi dorazí a neskončí ve spamu. Podbonosti najdete v článku 3 výhody SMTP serveru v cloudu.

Druhé vylepšení, které si můžete zkusit, spočívá v jednoduché ochraně před spamem. Mnohdy je zbytečné implementovat nějakou captchu a podobné věci. Stačí, když do formuláře přidáte políčko s nějakou kontrolní otázkou. Při zpracování formuláře, můžete do podmínky s isValid() zahrnout také vyhodnocení odpovědi v políčku s kontrolní otázkou a případně zprávu neodeslat a místo toho zobrazit přes FlashBag upozornění, že otázka nebyla zodpovězena správně.

Není to nic složitého, vše potřebné k tomu již umíte. Lepší postup by samozřejmě znamenal přidat další validovací pravidlo, které by vyhodnotila samotná funkce isValid(), ale o tom až někdy příště.

V dalším dílu seriálu se můžete těšit na zrychlení již tak dostatečně rychlého Silexu. Ukážeme si cacheování stránek, seskupování a minimalizaci CSS a JavaScriptu pomocí knihovny Assetic.

Reklama

Komentáře

Ahoj,
seriál o silexu mě tak zaujal, že jsem ho musel vyzkoušet. Bohužel při zprovoznění formuláře mi stále hlásí chybu:
RuntimeException in NativeSessionStorage.php line 144:
Failed to start the session because headers have already been sent by "/Applications/MAMP/htdocs/index.php" at line 1.

Vím, že ve wordpressu se tohle objevuje, pokud je chybně napsáno <? php s mezerou. Tady ovšem vůbec nevím co s tím. Ani strýček google nepomohl. Nedokázal by někdo poradit co by mohlo být špatně?

@zdenek: Ujistěte se, že před <?php nemáte mezeru, ani jiné znaky a za <?php následuje nový řádek.

Děkuji za odpověď, ale to jsem kontroloval jako první. Chyba se zdá být v něčem jiném - např. v pořadí jednotlivých app registrací nebo ve stažených componentách, ale nemůžu na chybu přijít.

V tom případě prověřte, jestli v indexu nemáte nějakou syntaktickou chybu, zda require_once obsahuje reálnou cestu (zda soubor existuje) a zda máte správně require_once v závorkách: require_once(__DIR__.'/../vendor/autoload.php');
Jiné řešení mne takhle obecně nenapadá, měl byste prohlédnout logy kvůli chybám v PHP.

v Sublime Text používám php checker, tak snad tam syntaktická chyba není - alespoň mi žádnou nenašel, require_once mám v závorkách. Jdu pátrat dál, děkuji za snahu.

Dívám se, že ani na oficiálních stránkách silexu není require_once v závorkách, tak to asi podmínka nebude. Každopádně chyba může být právě v require_once. Zkuste zadat úplnou cestu k souboru autoload.php, tedy např. /var/www/silex/vendor/autoload.php.
Popř. zkuste, zda Vám nastavení zobrazení chyb ve frameworku jak je uvedeno zde alespoň trochu nepomůže,

Všiml jsem si také, že píšete, že se chyba objevila po zprovoznění formuláře. Do té doby vše fungovalo?
Pokud ano, tak ověřte zmíněné "pořadí jednotlivých app registrací nebo ve stažených componentách"
<?php
require_once ...;
$app = new Silex\Application();
$app->register(...;
$app->register(...;
$app->register(...;

use ...;
use ...;
use ...;

$app->get(...;
$app->get(...;
# u kontaktu jak je uvedeno v článku:
$app->match('kontakt', function (Request $request) use ($app) {
$form = $app['form.factory']->createBuilder('form')
->add('name', 'text', array('attr' => array('class' => 'form-control') ))
->add('email', 'email', array('attr' => array('class' => 'form-control') ))
->add('subject', 'text', array('attr' => array('class' => 'form-control') ))
->add('message', 'textarea', array('attr' => array('class' => 'form-control', 'rows' => 10,) ))
->getForm();
if ('POST' == $request->getMethod()) {
$form->bind($request);
$data = $form->getData();
if ($form->isValid()) {
$body = $app['twig']->render('mail.html.twig', array('data' => $data));
$app['mailer'] = $app->share(function ($app) {
return new \Swift_Mailer($app['swiftmailer.transport']);
});
$message = \Swift_Message::newInstance()
->setSubject($data['subject'])
->setFrom($data['email'])
->setTo('jan.polzer@gmail.com')
->setBody($body);
$app['mailer']->send($message);
$app['session']->getFlashBag()->add('notice','Zpráva byla odeslána');
return $app->redirect('kontakt');
}
}
return $app['twig']->render('kontakt.html.twig', array(
'form' => $form->createView(),
));
})->bind('kontakt');

Vyřešeno takto: http://stackoverflow.com/questions/12643714/unit-testing-with-symfonys-f...

Stačí do index.php přidat:
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;

$app['session.storage'] = new MockArraySessionStorage();

Zdravím pánové, omlouvám se, že jsem vůbec nezareagoval, ale poslední dny jsem byl úplně zasekaný jinde. Každopádně fajn, že je vyřešeno. Já jsem v tom kódu s testy nepočítal, takže proto mi to funguje i bez MockArraySessionStorage.

Majitel Maxiorla. Nabízím mimo jiné placené poradenství pro Drupal. Jsem i na Twitteru.

Dobrý den, když v současné době postupuji podle uvedeného postupu, nemohu zprovoznit formuláře. Po delším bádáním jsem přišel na příčinu. Jsou jimi novější verzi knihoven. Uvedené příklady fungují jen s určitými verze, t.j. je nutné si pomocí composer.lock zafixovat konkrétní verze, jinak se vám nainstalují novější a formulář není možné vytvořit.

Aha, mrknu se na to, díky za info.

Majitel Maxiorla. Nabízím mimo jiné placené poradenství pro Drupal. Jsem i na Twitteru.

Přidat komentář