Parametry pre action – pouzivanie a validacia

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
westrem
Člen | 398
+
0
-

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
+
0
-

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 nepouziva empty 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.