Archiv rubriky: nette

Jak v NETTE změnit řazení tabulky klikem na záhlaví sloupců

Klasická situace, ale není to tak jednoduché jak se zdá. Co všechno musíme řešit?

  1. podle kterého sloupce řadíme
  2. jak je naposledy seřazen

Pokud toto už máme napsáno, tak dále musíme řešit různé překliky mezi sloupci.
Jdeme na věc!
Presenter:

< ?php
class AdminPresenter extends AdminBasePresenter {
 
	/** @persistent int */ // označuje proměnou níže jako persistentní - proměnná se sama přenáší mezi kliky v rámci presenteru (náhrada bezestavovosti protokolu HTTP)
	public $p_sort = 1; //  proměnná pro uložením posledního řazeného sloupce
 
	/* 
	 * $sort Integer - pořadové číslo sloupce z dotazu podle kterého se má řadit
	 * $by String - způsob řazení
	 */ 
	public function renderTabulka($sort = 1, $by = 'ASC') { 
		$by = ($sort != $this->p_sort) ? 'ASC' : $by; // zjistíme jestli řadíme stejný sloupec jako minule
		$this->template->rows = dibi::fetchAll('SELECT ... FROM ... ORDER BY %i %sql', $sort, $by); // seřadíme
		$this->template->by = ($by == 'ASC') ? 'DESC' : 'ASC'; // pošleme proměnou s opačným způsobem řazení do templatu 
		$this->p_sort = $sort; // uložíme sloupec, podle kterého jsme řadili
	}

Template:

<table>
<tr>
	<td><a href="{plink sklad, 1, $by}">Sloupec 1</a></td>
	<td><a href="{plink sklad, 2, $by}">Sloupec 2</a></td>
	<td><a href="{plink sklad, 3, $by}">Sloupec 3</a></td>
</tr>
{if count($rows) > 0}
	{foreach $rows as $row}
		<tr>
			<td> {$row['sloupec1']} </td>
			<td> {$row['sloupec2']} </td>
			<td> {$row['sloupec3']} </td>
		</tr>
	{/foreach}
{else}
    <tr><td colspan="3">V databázi se nenalézá žádný záznam.</td></tr>
{/if}
</table>

Doufám, že to je pochopitelné :).

SPRÁVNÉ ŘEŠENÍ PRO: Přímé stažení souboru v NETTE pomocí redirectu formuláře

Již dříve jsem nastínil jak na to. Ale to bylo řešení postavené na dříve osvojených principech (si všechno napsat sám) a až nyní jsem našel způsob, jak to napsat elegantněji pomocí NETTE.

Presenter:

< ?php
 
class PcClientPresenter extends BasePresenter
{
	protected function createComponentForm($name) {
		$form = new AppForm($this, $name);
		$form->addPassword('heslo', 'Heslo: ')
			->addRule(Form::FILLED, 'Heslo musí být vyplněné.');
		$form->addSubmit('send', 'Stáhnout soubor »');
 
		$form->onSubmit[] = array($this, 'formSubmitted');
	}
 
	public function formSubmitted($form) {
		$file = WWW_DIR . '/data/soubor.zip'; // soubor může být úplně mimo web root (=nestáhnutelný pomocí URL)
		$fileName = 'Soubor po stažení.zip'; // název pod kterým se bude soubor stahovat uživateli
 
		$arr = $form->getValues();
		if ($arr['heslo'] === 'HESLO') {
			$httpResponse = Environment::getHttpResponse();
			$httpResponse->setHeader('Pragma', "public");
			$httpResponse->setHeader('Expires', 0);
			$httpResponse->setHeader('Cache-Control', "must-revalidate, post-check=0, pre-check=0");
			$httpResponse->setHeader('Content-Transfer-Encoding', "binary");
			$httpResponse->setHeader('Content-Description', "File Transfer");
			$httpResponse->setHeader('Content-Length', filesize($file));
			$this->sendResponse(new DownloadResponse($file, $fileName, array('application/octet-stream', 'application/force-download', 'application/download')));
		} else {
			$this->flashMessage('Nesouhlasí zadané heslo.', 'error');
		}
	}
 
	public function actionDefault() {}
}

Pro pořádek připojuji i obsah šablony:

{block #content}
<h1>Stažení PC klienta</h1>
    <div class="flash {$flash->type}">{$flash->message}</div>
    {control form}
{/block}

Přímé stažení souboru v NETTE pomocí redirectu formuláře

Potřeboval jsem udělat jednoduché stahovadlo souboru. Požadavky byly, aby pro stažení se muselo zadat heslo a aby soubor nešlo ani následně stahovat přes nějakou konstantní URL.

Níže uváděné řešení již považuji za zastaralé a nechávám ho tady jen aby bylo možné porovnat, že i toto jde v NETTE napsat lépe. Pokračujte zde…


Řešení je nakonec triviální:
Presenter:

< ?php class PcClientPresenter extends BasePresenter {
 	protected function createComponentForm($name) {
 		$form = new AppForm($this, $name);
 		$form->addPassword('heslo', 'Heslo: ')
			->addRule(Form::FILLED, 'Heslo musí být vyplněné.');
		$form->addSubmit('send', 'Stáhnout soubor »');
 
		$form->onSubmit[] = array($this, 'formSubmitted');
	}
 
	public function formSubmitted($form) {
		$file = WWW_DIR . '/data/soubor.zip'; // soubor může být úplně mimo web root (=nestáhnutelný pomocí URL)
		$fileName = 'Soubor po stažení.zip'; // název pod kterým se bude soubor stahovat uživateli
 
		$arr = $form->getValues();
		if ($arr['heslo'] === 'HESLO') {
			$httpResponse = Environment::getHttpResponse();
			// Pošleme prohlížeči hlavičky pomocí kterých mu řekneme že se jedná o stahování
			$httpResponse->setContentType('application/octet-stream');
			$httpResponse->setContentType('application/force-download');
			$httpResponse->setContentType('application/download');
			$httpResponse->setHeader('Pragma', "public");
			$httpResponse->setHeader('Expires', 0);
			$httpResponse->setHeader('Cache-Control', "must-revalidate, post-check=0, pre-check=0");
			$httpResponse->setHeader('Content-Transfer-Encoding', "binary");
			$httpResponse->setHeader('Content-Description', "File Transfer");
			$httpResponse->setHeader('Content-Length', filesize($file));
			$httpResponse->setHeader('Content-Disposition', 'attachment; filename="' . $fileName . '"');
 
			// v cyklu načítáme postupně soubor a posíláme ho prohlížeči (u malých souborů by to šlo najednou ale u velkých by došla dostupná paměť pro PHP)
			$fp = fopen($file, 'r');
			while (!feof($fp)) {
				echo fread($fp, 65536);
				flush();
			}
			fclose($fp);
			$this->terminate();
		} else {
			$this->flashMessage('Nesouhlasí zadané heslo.', 'error');
		}
	}
 
	public function actionDefault() {}
}

Template:

{block #content}
<h1>Stažení PC klienta</h1>
<div class="flash {$flash->type}">{$flash->message}</div>
{control form}
{/block}


Vše!

Jak se odkazovat na předchozí a následující příspěvek

V tomto příspěvku mi nejde o klasické stránkování, které se dá snadno vyřešit pomocí klauzule LIMIT v MySQL.
Nejde mi ani o seznamy, které se dají jednoduše a striktně seřadit podle velikosti (číslo, čas), ale jde mi o případ, kdy máme například seznam článků seřazených podle jména a chceme jednoduše přecházet na další či předchozí. Tady řazení podle operátorů <, > nebude to pravé.
Vyřešit se to dá následovně (vykopírováno z NETTE aplikace s použitou knihovnou DIBI):

// funkce zobrazuje objednávku dle ID, pokud ji pošleme parametr KAM, tak se zpracuje a funkce se zavolá znova jen s konkrétní ID
 
public function renderDetail($id, $kam = '') {
	if ($kam == '') {
		$this->template->rows = dibi::fetchAll('SELECT * FROM [objednavka_polozky] [o] JOIN [w_zbozi] USING ([kod_zbozi]) JOIN [objednavka_hlavicka] USING ([id_objednavka]) WHERE [id_objednavka] = %i GROUP BY [kod_zbozi]', $id);
	} elseif ($kam == 'PREV') {
		dibi::query('SET @poradi:=0, @poradinow:=0, @idprev:=0, @idnext:=0;');
		dibi::query('SELECT @poradi:=@poradi+1, IF(id_objednavka=%i, @poradinow:=@poradi, 0) FROM [objednavka_hlavicka] [oh] ORDER BY [cas] DESC', $id);
		dibi::query('SET @poradi:=0;');
		dibi::query('SELECT @poradi:=@poradi+1, IF(@poradinow-1=@poradi, @idprev:=id_objednavka, 0), IF(@poradinow+1=@poradi, @idnext:=id_objednavka, 0) FROM [objednavka_hlavicka] [oh] ORDER BY [cas] DESC');
		$row = dibi::fetch('SELECT @poradinow, @idnext AS next, @idprev AS prev;');
		if ($row['prev'] == 0) {
			$this->flashMessage('Nelze se již posunout na předchozí objednávku.', 'info');
			$this->redirect('Admin:detail', array('id' => $id));
		}
		$this->redirect('Admin:detail', array('id' => $row['prev']));
	} elseif ($kam == 'NEXT') {
		dibi::query('SET @poradi:=0, @poradinow:=0, @idprev:=0, @idnext:=0;');
		dibi::query('SELECT @poradi:=@poradi+1, IF(id_objednavka=%i, @poradinow:=@poradi, 0) FROM [objednavka_hlavicka] [oh] ORDER BY [cas] DESC', $id);
		dibi::query('SET @poradi:=0;');
		dibi::query('SELECT @poradi:=@poradi+1, IF(@poradinow-1=@poradi, @idprev:=id_objednavka, 0), IF(@poradinow+1=@poradi, @idnext:=id_objednavka, 0) FROM [objednavka_hlavicka] [oh] ORDER BY [cas] DESC');
		$row = dibi::fetch('SELECT @poradinow, @idnext AS next, @idprev AS prev;');
		if ($row['next'] == 0) {
			$this->flashMessage('Nelze se již posunout na následující objednávku.', 'info');
			$this->redirect('Admin:detail', array('id' => $id));
		}
		$this->redirect('Admin:detail', array('id' => $row['next']));
	} else {
		//chyba
		$this->redirect('Admin:objednavky');
	}
}

Příklad není úplně přehledný, protože jsem ho vykopíroval z hotové aplikace a aplikační logika není úplně čistá, ale pro demonstraci to doufám stačí.

Automatické dotahování údajů o podnikatelích z databáze ARES

Někde na internetu jsem viděl jak po zadání IČ se zbytek údajů o živnostníkovi/firmě dotáhl sám. A jelikož jsem si chtěl o tuto možnost rozšířit i své personální účetnictví, tak jsem pátral jak na to.

O databázi ARES jsem se dozvěděl již dříve, ale nikdy jsem se nedostal k samotné implementaci. Až dneska jsem narazil na článek Radka Hulána, řešící přesně toto a tak jsem si řekl, že se na to podívám.

Zdroj dat: Databáze ARES
Script: MyEGO blog

Script po úpravě na straně serveru (jedná se o NETTE akci):

public function handleLoadInfo($IC) {
	$this->payload->firma = array();
    	// dá se vybrat hned z několika zdrojů dle potřeby http://wwwinfo.mfcr.cz/ares/ares_xml.html.cz#k3
	define('ARES','http://wwwinfo.mfcr.cz/cgi-bin/ares/darv_bas.cgi?ico=');
	$ico = intval($IC);
	// nemohl jsem použít kvůli omezení na serveru, nahradil jsem pomocí CURL
	//$file = @file_get_contents(ARES.$ico);
	if ($curl = curl_init(ARES.$ico)) {
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
		$content = curl_exec($curl);
		//$info = curl_getinfo($curl);
		curl_close($curl);
		$xml = @simplexml_load_string($content);
	}
	$a = array();
	if (isset($xml)) {
		$ns = $xml->getDocNamespaces();
		$data = $xml->children($ns['are']);
		$el = $data->children($ns['D'])->VBAS;
		if (strval($el->ICO) == $ico) {
			$a['ico'] 	= strval($el->ICO);
			$a['dic'] 	= strval($el->DIC);
			$a['firma'] = strval($el->OF);
			$a['ulice']	= strval($el->AA->NU).' '.strval($el->AA->CD).'/'.strval($el->AA->CO);
			$a['mesto']	= strval($el->AA->N).'-'.strval($el->AA->NCO);
			$a['psc']	= strval($el->AA->PSC);
			$a['stav'] 	= 'ok';
		} else {
			$a['stav'] 	= 'IČ firmy nebylo nalezeno';
		}
	} else {
		$a['stav'] 	= 'Databáze ARES není dostupná';
	}
	$this->payload->firma = $a;
	$this->sendPayload();
}

Script po úpravě na straně uživatele:

<script type="text/javascript">
    <!--
	$('#frmkontakt-IC').change(function(event) {
		$.getJSON({link loadInfo!}, {'IC': $('#frmkontakt-IC').val()}, function(payload) {
			if (payload.firma.stav == 'ok') {
				$('input[name=DIC]').val(payload.firma.dic);
				$('input[name=prijmeni]').val(payload.firma.firma);
				$('input[name=ulice]').val(payload.firma.ulice);
				$('input[name=mesto]').val(payload.firma.mesto);
				$('input[name=psc]').val(payload.firma.psc);
			} else {
				alert(payload.firma.stav);
			}
		});
	});
	-->
</script>

Chtělo by to ještě doladit o podmínky kdy co a jak vypisovat, spojovat (číslo orientační a popisné, město a čtvrť, atd.), ale jako referenční implementace to je dobrý základ.

Jak na potvrzení registrace v NETTE?

U jednoho dříve zveřejněného příspěvku (Registrace uživatelů pomocí NETTE) se objevil komentář žádající vysvětlení, jak dále zpracovávat potvrzující odkaz z došlého mailu.
Pro nevytržení z kontextu zopakuji část, která takový odkaz tvoří a odesílá e-mail. Kód jsem upravil o dynamickou tvorbu odkazu, jak jsem byl upozorněn v diskuzi u dříve zmíněného článku.

// vytvoření kontrolního hashe
$kontrolni_link = sha1($values['nick'] . time() . 'e743dd075ff52c2');
 
unset($values['pass2']); // druhé heslo neukládáme
$values['retik'] = $kontrolni_link;
dibi::query('INSERT INTO [user]', $values);
 
#mail pro kontrolu
$mail = new Mail;
$mail->setFrom('Registrace na webu xxx.com <info @xxx.com>');
$mail->addTo($values['email']);
$mail->setSubject('Registrace na webu xxx.com');
$mail->setBody("Děkujeme za Váši registraci. Potvrďte prosím na adrese " . $this->link('//Registrace:kontrola', array('kontrola'=>$kontrolni_link)) . ".");
$mail->send();
// zase mi tu WP zlobí dále už nic nemá být ;-)
</info>

Toto je jen jedna možnost jak kontrolu dělat. Díky způsobu tvorby (používáme funkci time() a její hodnotu si nikam dále neukládáme) kontrolního HASHe, je nutné ho uložit do databáze a následně při kontrole ho testovat zda odpovídá. Jen pro informaci zmíním, že je tu i další možnost tvorby a to, že si odkazem pošleme více parametrů, díky kterým budeme schopni registraci potvrdit, bez dopomocí databáze (např.: $this->link(‚//Registrace:kontrola‘, array(‚nick’=>$values[‚nick‘], ‚kontrola’=>sha1($values[‚nick‘] . ‚e743dd075ff52c2‘)))).

A nyní samotné MINIMALISTICKÉ (=jedná se o koncept, ne o deklaraci dokonalého zpracování) zpracování (budu se držet první možnosti):

if (empty ($_GET['kontrola'])) {
	die('CHYBA!');
} else {
	$kod = $_GET['kontrola'];
	dibi::query('UPDATE [user] SET [retik]="" WHERE [retik]=%s', $kod);
 
	if (dibi::affectedRows() == 1) {
		echo 'Registrace dokončena.';
	} else {
		echo 'Registraci se nepodařilo dokončit.';
	}
}

Nutno podotknout, že při přihlašování mám podmínku na nevyplněnost sloupce retik v tabulce user.

...
$row = dibi::fetch('SELECT [id], [nick], [pass], [retik], [role] FROM [user] WHERE [nick]=%s', $username);
 
if ($row->retik !== '') {
    throw new AuthenticationException("Registrace nebyla dosud potvrzena.", self::NOT_APPROVED);
}
...

Celá ukázka nevyužívá plné síly NETTE. Ukázka vznikla jako návaznost na původní článek, kde se NETTE použilo v již hotové aplikaci. Doufám ale, že postup je z popisu jasný a přepsání do plného NETTE nebude činit problém.

Taky vás nebaví stále dokola psát chyby validací v NETTE FORMS?

Jistě to všichni známe. Stále dokola se opakující zprávy: „Pole „Jméno“ musí být vyplněno!“, „Pole „Příjmení“ musí být vyplněno!“, … A co když najednou se nám tato zpráva přestane líbit a chceme ji nahradit? Nezbývá nic jiného než všechny hlášky projít a přepsat je.

Ale ono to jde i jednodušeji. Stačí třeba v bootstrapu si připravit masku těchto zpráv a je vystaráno.

Rules::$defaultMessages = array(
	Form::FILLED => 'Položka „%label“ musí být vyplněna.',
	Form::EMAIL => 'Položka „%label“ nemá správný formát.',
	Form::URL => 'Položka „%label“ nemá správný formát.',
	Form::MAX_LENGTH => 'Položka „%label“ je příliš dlouhá.',
	Form::INTEGER => 'Položka „%label“ musí být celé číslo.',
	Form::REGEXP => 'Neplatný formát položky „%label“.',
);

V presenteru pak stačí…

...
$form->addText("name", "Jméno:")
	->addRule(Form::FILLED);
...

Generování vícejazyčného formuláře v NETTE

Občas je potřeba mít vícejazyčnou strukturu menu (kategorií, …) s tím, že předem nevím kolik jazyků budu potřebovat, ale zároveň chci aby administrace s tím počítala.
Jak takový automaticky generovaný formulář napsat v NETTE?

Struktura tabulek databáze:

Presenter (továrnička na generování formuláře):

public function createComponentCat() {
	// vytáhneme si všechny jazyky
	$langs = dibi::fetchAll('SELECT [id], [code] FROM [languages] ORDER BY [code]');
 
	$form = new AppForm;
	// * (budu se na toto místo dále odkazovat v textu)
	foreach ($langs as $lang) {
		$form->addText("name_{$lang['id']}", "Jméno kategorie [{$lang['code']}]:")
		    ->addRule(Form::FILLED);
	}
	// *
 
	// tyto skryté pole se budou hodit pro editaci
	$form->addHidden('id');
	$form->addHidden('parent_id');
	$form->addSubmit('send', ' Uložit ');
 
	$form->onSubmit[] = array($this, 'formCatSubmitted');
 
	return $form;
}

* – tady mohou pro více polí nastat dvě situace:
1/ chceme jazykově oddělené atributy mít u sebe
Pak musíme pro každý atribut vložit tento cyklus. Takže pět atributů znamená pět pod sebou těchto cyklů.
2/ chceme atributy seskupovat podle jazyků
V tomto případě nám stačí jeden cyklus a v něm nasázené všechny atributy.

O výkonu zde nemá cenu se dohadovat. Jedná se o administraci a tam nikdy nebude velký nával.

Presenter (zpracování formuláře):

public function formCatSubmitted($form) {
	$val = $form->getValues();
	# TABLE: categories
	$categories['id'] = $val['id'];
	$categories['parent_id'] = $val['parent_id'];
 
	if ($categories['id'] == 0) { // jedná se o nový záznam
		// vytáhneme si level vkládaného záznamu
		// level je hloubka zanoření ve stromové struktuře
		$categories['level'] = ($categories['parent_id'] == 0) ? 1 : dibi::fetchSingle('SELECT [level]+1 FROM [categories] WHERE [id]=%i LIMIT 1', $categories['parent_id']);
		// vytáhneme si position vkládaného záznamu (u nového záznamu budeme vkládat novou kategorii až na konec)
		// position je pořadí v dané kategorii
		$categories['position'] = dibi::fetchSingle('SELECT [position]+1 FROM [categories] WHERE [parent_id]=%i ORDER BY [position] DESC LIMIT 1', $categories['parent_id']);
		// oprava při nule pokud vkládáme první záznam
		$categories['position'] = ($categories['position'] == 0) ? 1 : $categories['position'];
		dibi::query('INSERT INTO [categories]', $categories);
		$categories_translations['categories_id'] = dibi::insertId();
	} else {
		dibi::query('UPDATE [categories] SET', $categories, 'WHERE [id]=%i LIMIT 1', $categories['id']);
		$categories_translations['categories_id'] = $categories['id'];
	}
 
	# TABLE: categories_translations
	$langs = dibi::fetchAll('SELECT [id] FROM [languages]');
 
	if ($categories['id'] == 0) { // jedná se o nový záznam
		foreach ($langs as $lang) {
			$categories_translations['language_id'] = $lang['id'];
			$categories_translations['name'] = $val["name_{$lang['id']}"];
			dibi::query('INSERT INTO [categories_translations]', $categories_translations);
		}
	} else {
		$categories_id = $categories_translations['categories_id'];
		unset($categories_translations['categories_id']);
		foreach ($langs as $lang) {
			$categories_translations['name'] = $val["name_{$lang['id']}"];
	    		dibi::query('UPDATE [categories_translations] SET', $categories_translations, 'WHERE [language_id] = %i AND [categories_id] = %i LIMIT 1', $lang['id'], $categories_id);
 
			// tady je ještě třeba vyřešit situaci, kdy již máme naplněnou databázi kategoriemi a přidáme nový záznam do tabulky jazyků. Update by nebylo nad čím provést, proto musíme nové záznamy pro nové jazyky INSERTnout do tabulky překladů.
			// zjistíme si, zda nám update něco ovlivnil (ANO - záznam existuje, NE - jedná se o nový záznam)
			$info = dibi::getConnection()->driver->getInfo();
			if ($info['Rows matched'] === 0) {
				$categories_translations['language_id'] = $lang['id'];
				$categories_translations['categories_id'] = $categories_id;
				dibi::query('INSERT INTO [categories_translations]', $categories_translations);
			}
		}
	}
 
	$this->flashMessage('Data uložena.', 'info');
	$this->redirect('Category:default', $this->getParam('parent_id'));
}

A teď už jen pro pořádek zbývá uvést metody presenteru Add a Edit:

public function renderAdd($parent_id) {
	$this['cat']->setDefaults(array('id' => 0, 'parent_id' => $parent_id));
	$this->template->form = $this['cat'];
}
 
public function renderEdit($id, $parent_id) {
	$this->setView('add');
 
	// vytáhneme si data
	$rows = dibi::fetchAll('SELECT *, [b].[id] AS [bid] FROM [categories_translations] [a] JOIN [categories] [b] ON [a].[categories_id] = [b].[id] WHERE [b].[id] = %i ORDER BY [language_id]', $id);
 
	// začneme generovat pole pro naplnění formuláře pro editaci
	$arr = array();
	foreach ($rows as $row) {
		$arr["name_{$row['language_id']}"] = $row['name'];
	}
	$arr['id'] = $row['bid'];
	$arr['parent_id'] = $row['parent_id'];
 
	$this['cat']->setDefaults($arr);
	$this->template->form = $this['cat'];
}

Výsledek takového formuláře pak vypadá pro tři jazyky a více polí třeba takto:

Validace uživatelského jména v NETTE

Většinou chceme aby uživatelské jméno bylo složeno jen z písmen, čísel a maximálně dovolíme znaky „-“ a „_“. V NETTE takový prvek formuláře můžeme definovat následovně:

$form->addText('username', 'Uživatelské jméno')
	->addRule(Form::REGEXP, '„Uživatelské jméno“ musí začínat písmenem a může obsahovat jen písmena, čísla a znaky "-", "_".', '/^[a-z][a-z0-9_-]+$/i');

Vysvětlení regulárního výrazu:
/^ – značí začátek
[a-z] – očekáváme znak a-z či A-Z
[a-z0-9_-] – očekáváme znaky a-z, A-Z, 0-9, znaky „-“ a „_“
+ – váže se k předchozí definici a říká nám, že znak definovaný pomocí [a-z0-9_-] se musí vyskytovat 1 až n-krát (znak * by nám naznačoval že výskyt může být i 0)
$/ – značí konec řetězce
i – značí, že celý výraz je CASE INSENSITIVE neboli že nezáleží na velikosti písmen. Tudíž nemusíme psát [a-zA-Z] ale stačí nám jednoduché [a-z]

Tento výraz nám v tomto zápisu ještě zajistí, že Uživatelské jméno bude dlouhé minimálně 2 znaky. Samozřejmě se dá regulárním výrazem řešit i povinná délka, ale dle mě už pak nejsme schopni předat uživateli přesnou odpověď kde v zadání udělal chybu.

Captcha pro NETTE 2.0 (PHP 5.2)

Dneska jsem narazil na problém s přidáním Captchy do NETTE. Plugin z Nette Addons je nefunkční a dle vlákna na NETTE fóru se již delší dobu o to nikdo nestará. Pročítáním vlákna dále jsem narazil na aktualizovanou verzi od Pandy, ale ten to má zase optimalizované pro PHP 5.3. Takže jsem dále jel chybu po chybě vesměs dle fóra a nakonec jsem odstranil poslední vadu a zde přikládám výsledek.

NETTE 2.0, Captcha for PHP 5.2