Parametry pre action – pouzivanie a validacia
- westrem
- Člen | 398
Zdravim,
mam snahu spravit si vlastnu validaciu vstupnych parametrov pre rozne actions a
views a to pomocou anotacii.
Kym sa do samotnej implementacii ale pustim rad by som si ujasnil ako Nette
vlastne naraba s parametrami a to jednak s tymi uvedenimi ako argumenty pri
napr action
a persistentnymi.
Aby vsak nedochadzal k omylom, nastudovane mam v podstate vsetko co som vedel dohladat na strankach Nette, primarne teda dokumentaciu k Presenteru
Ide mi skor o odkazy priamo do kodu na API, kde sa deje nacitavanie jednak
persistentnych parametrov (to ak sa nemylim sa deje pomocou
loadState()
?) a druhorak o $_GET
a $_POST
parametry. Kde si ich Nette taha a pracuje s nimi? Nemozem sa k tomu miestu
v kode dopatrat.
Tiez mi nie je jasne, ako Nette vie urcit nasledovne: majme tuto action:
// vnutro HomePresenter-u
public function actionShow($limit = 10, $offset = 0) { .. }
Dalej majme generovanie linku:
{plink :Home:show, 'offset' => 20}
Vie mi niekto povedat, kde konkretne v kode dochadza k rozpoznaniu, ze
$limit
je nepovinny a k spravnemu napasovaniu uvedenych a neuvedenych parametrov? Stale
v tom citim urcitu magiu.
Dalej, rozmyslal som kam vobec umiestnit samotnu validaciu parametrov, a
jedine miesto, ktore ma napada a malo by byt aj z hladiska navrhu spravne je
startup
funkcia od BasePresenter
. Alebo mate
lepsi navrh?
PS: Ak by to bolo mozne, mohli by ste linkovat na API k verzii 0.9.x a nie k najnovsej 2. verzii.
PS2: Rad by som sa vyhol offtopic prispevkom ohladne validacie, ci ju robit az v modeli, ci vobec validovat alebo ju robit uz takto low-level na parametroch, to je v konecnom dosledku vec programatora a ja osobne preferujem v tomto pripade low-level uz na urovni parametrov a nie na urovni modelu.
Vopred dakujem za akekolvek nasmerovanie a hinty.
- westrem
- Člen | 398
Tak nakoniec sa mi podarilo najst pre mna dolezite miesta v kode a vysledok je podla mna hodne silna validacia vstupnych parametrov.
Co sa validuje
- persistentne parametry
- parametry akcie/render metody
V oboch pripade sa validuje ci uz uvedena hodnota v URL alebo defaultne
uvedena hodnota v zdrojaku. V pripade, ze sa validacia nepodari vyhodi sa
Exception, takze v ErrorPresenter si ju mozete
odchytit a podla nej nastavit napr nejaky pekny View s chybovou hlaskou.
Zapis
Validacne pravidla sa zapisuju anotaciami nasledovnym
stylom: @validate($var, callback [, $arg, $arg ..])
Ich umiestnenie je mozne bud pred metodou actionXY
alebo
renderXY
. Ak vsak existuju obe musia byt anotacie
uvedene pred metodou actionXY
.
Priklad:
/**
* @param int $id
* @validate($id, %i)
* @validate($id, >=, 6)
* @param string $color
* @validate($color, {}, red, green, blue)
* @param bool $render
*
* @nullable($render)
*/
public function actionXY() { .. }
Vyssie uvedene znamene presne to by ste si mysleli:
$id
musi byt cislo vecsie rovne ako 6$color
moze byt jedna z hodnot ‚red‘, ‚green‘, ‚blue‘$render
moze byt prazdny (nadobudat prazdnu hodnotu – na jej detekovanie sa nepouzivaempty
ale sofistikovanejsi sposob)
Specialitka
Neviem nakolko kto vyuziva persistentne parametre, ja som sa vsak obcas
dostal do situacie, ked som mal v presentere jeden persistentny parameter
$id
, ktory vsak sluzil roznym actions a vzdy na
tak trochu iny ucel (mohol nadobudat rozne typy hodnot).
Validacia pamata aj na tuto situaciu a je mozne nieco nasledovne:
final SamplePresenter extends BasePresenter
{
/** @persistent int */
$id;
/**
* @validate($id, {}, 2, 4, 6)
* Tu moze byt $id len jedno z cisiel 2, 4 alebo 6
*/
public actionX() { .. }
/**
* @validate($id, %i)
* Tu musi byt $id obecny integer
*/
public actionY() { .. }
/**
* @param bool $id
* Tu sme spravili pretypovanie z int uvedeneho na zaciatku na bool
*/
public actionY() { .. }
}
Kesovanie
Aby sa urychlila validacia, resp spracovavanie anotacii, su tieto spracovane iba raz a potom su kesovane. Kes je nastavena tak, aby sa invalidovala pri kazdej zmene .php subory, ktory obsahuje definiciu presenteru.
Implementacia
final private function validateParameters()
{
// common regular expressions
static $phpIdentifier = '[_a-zA-Z\x7F-\xFF][_a-zA-Z0-9\x7F-\xFF]*';
static $phpTypes = 'int|integer|bool|boolean|float|double|real|string|array|object|mixed';
// ---------------------------------------------------------------------------------------------------------------------------
// 1. try cache
// ---------------------------------------------------------------------------------------------------------------------------
$cache = NEnvironment::getCache('application/parameters.validation');
$cacheKey = $this->getAction(TRUE);
if (isset($cache[$cacheKey])) {
$params = $cache[$cacheKey]['params'];
$dValues = $cache[$cacheKey]['dValues'];
$nullable = $cache[$cacheKey]['nullable'];
}
else {
$r = $this->getReflection();
$actionMethod = $this->formatActionMethod($this->getAction());
$renderMethod = $this->formatRenderMethod($this->getAction());
$method = ($r->hasMethod($actionMethod)) ? $actionMethod : (($r->hasMethod($renderMethod)) ? $renderMethod : NULL);
$annotations = $r->getMethod($method)->getAnnotations();
if (!isset($annotations['validate'])) { return; }
// ---------------------------------------------------------------------------------------------------------------------------
// 2. build params & default values
// ---------------------------------------------------------------------------------------------------------------------------
$params = array();
$dValues = array();
$nullable = (isset($annotations['nullable'][0])) ? (array) $annotations['nullable'][0] : array();
// a) persistent params
foreach ($this->getReflection()->getPersistentParams() as $name => $param) {
if (isset($param['def'])) {
$dValues[$name] = $param['def'];
}
$ann = $this->getReflection()->getProperty($name)->getAnnotation('persistent');
preg_match('/(' . $phpIdentifier . '){0,1}/', $ann, $matches);
if (empty($matches)) { continue; }
$params['$' . $name] = array(
'type' => $matches[1],
);
}
// b) method params
$mParams = $r->getMethod($method)->getParameters();
foreach ($mParams as $param) {
if ($param->isDefaultValueAvailable()) {
$dValues[$param->getName()] = $param->getDefaultValue();
}
}
// c) annotated params
foreach ((isset($annotations['param'])) ? $annotations['param'] : array() as $param) {
preg_match('/(' . $phpIdentifier . '){0,1}\s*(\$' . $phpIdentifier . ')/', $param, $matches);
if (empty($matches)) { continue; }
$params[$matches[2]] = array(
'type' => $matches[1],
);
}
// ---------------------------------------------------------------------------------------------------------------------------
// 3. attach validation rules
// @validate($var, $callback [, $arg, $arg, ..])
// ---------------------------------------------------------------------------------------------------------------------------
foreach ($annotations['validate'] as $rule) {
$identifier = $rule[0];
$validator = $rule[1];
$args = array_slice((array) $rule, 2);
if (!isset($params[$identifier])) { continue; }
switch (TRUE) {
// compare type fallback
case PValidate::isCompareType($validator) && (count($args) == 1) :
$args[] = $validator;
if (in_array($params[$identifier]['type'], array('int', 'integer', 'float', 'double', 'real',))) {
$validator = PValidate::NUMERIC_CMP;
}
elseif ($params[$identifier]['type'] == 'string') {
$validator = PValidate::STRING_CMP;
}
else {
$validator = PValidate::COMPARE;
}
break;
// range type fallback
case PValidate::isRangeType($validator) :
$args[] = $validator;
$validator = ($params[$identifier]['type'] == 'string') ? PValidate::STRING_RANGE : PValidate::RANGE;
break;
// default callback processing
case is_callable($validator): break;
// Validate fallback
default:
$validator = PValidate::of($validator);
break;
}
$params[$identifier]['validators'][] = array(
CALLBACK => $validator,
ARGUMENTS=> $args,
);
}
// ---------------------------------------------------------------------------------------------------------------------------
// 4. save to cache
// Depends on Presenter .php file
// ---------------------------------------------------------------------------------------------------------------------------
$application = NEnvironment::getApplication();
$requests = $application->getRequests(); $request = end($requests);
$cache->save(
$this->getAction(TRUE),
array(
'params' => $params,
'dValues' => $dValues,
'nullable' => $nullable,
),
array(
'files' => array(
$application->getPresenterLoader()->formatPresenterFile($request->getPresenterName()),
),
)
);
}
// ---------------------------------------------------------------------------------------------------------------------------
// 5. perform validation
// ---------------------------------------------------------------------------------------------------------------------------
foreach ($params as $param => $meta) {
if (!isset($meta['validators'])) { continue; }
$identifier = substr($param, 1); // get rid of $
$defVal= (isset($dValues[$identifier])) ? $dValues[$identifier] : NULL;
$value = $this->getParam($identifier, $defVal);
if (((string) $value == '') && in_array($param, $nullable)) { continue; } // do not validate nullable null parameters
if (!PValidate::conditions($value, $meta['validators'])) {
throw new PBadParamRequestException('Param ' . $param . ' is not valid');
}
}
}
Pouzitie
Danu funkciu si mozete dat napr do BasePresenteru
a volat ju pri
startup
metode takto:
abstract class BasePresenter extends NPresenter
{
public function startup()
{
parent::startup();
// parameters validation
$this->validateParameters();
}
}
Known issues
Pri implementovani ma zarazilo par veci, ktore Nette robi a jedna z nich sa mi nepodarila obist (bol by nutny zasah do FW). Ide o to, ze ak persistentnemu parametru nastavite nejaku default hodnotu napr:
/** @persistent */
$id = 10;
Nette si zapameta, ze dany parameter bol typu integer
a
automaticky potom nastavuje tento typ akejkolvek hodnote parametru cim
znemoznuje pouzitie roznorodych validacii pre persistentny parameter ako je to
uvedene v sekcii „Specialitka“
Zaverom
V uvedenej implementacii je pouzitych vela mojich internych classes, nemal
som vsak tendenciu to z ukazkoveho kodu mazat – sluzi to aspon k tomu aby
ludia popustili uzdu fantazie a videli co vsetko je mozne docielit.
Taky zapis <=, 30
je podla mna ovela citatelnejsi ako
someNumberComparingFunction, <=, 30
.
Cely priklad ma preto posluzit najme ako zakladna kostra implementacie a miesto jej vyuzitia v Nette, kazdy si ju moze upravit a zmenit ako uzna za vhodne.
Dalej uz nejaku dobu premyslam, ze zverejnim niektore svoje validacne a helper classy a tym padom by mohla komunita pouzivat aj vyssie uvedene zapisy – toto vsak moze a nemusi nastat, zavysi ci budem mat cas na prepisanie danych tried do navonok konzsistentne pouzitelneho packagu.