Napovídání skalárních typů přetěžováním

Školení, která pořádám

V předchozím článku jsem popsal, jak se dá emulovat neexistence napovídání skalárních typů u parametrů funkcí, a slíbil jsem popsat ještě jedno řešení.

To řešení spočívá v tom, že místo přímého volání funkce zavoláme obálku, která zkontroluje a převede typ parametrů a teprve poté funkci zavolá. Výhoda tohoto řešení spočívá v tom, že není potřeba na začátek každé funkce doplňovat kód provádějící kontrolu parametrů.

U funkcí by volání kódu např. call_check('smtp_commands', array($fp, $commands)) nebylo příliš pěkné, ale u objektů se dá dobře využít přetěžování metod.

<?php
class CallCheck {
    /** Převod a kontrola parametrů podle dokumentačních komentářů
    * @param string $name název metody - volaná metoda je uvozena podtržítkem
    * @param array $arguments pole parametrů
    * @return mixed návratová hodnota volané metody
    */
    function __call($name, $arguments) {
        $function = get_class($this) . "::$name";
        if (!method_exists($this, "_$name")) {
            trigger_error("Call to undefined method $function()", E_USER_ERROR);
            return false;
        }
        $reflection = new ReflectionMethod(get_class($this), "_$name");
        preg_match_all("~^[ \t]*\\*[ \t]*@param[ \t]+(\\S+)[ \t]+\\\$(\\S+)~m", $reflection->getDocComment(), $matches, PREG_SET_ORDER);
        foreach ($arguments as $i => $val) {
            if (!isset($matches[$i])) {
                // missing doc comment
            } elseif (in_array($matches[$i][1], array("boolean", "bool", "integer", "int", "float", "double", "string", "object", "null"))) {
                settype($arguments[$i], $matches[$i][1]);
            } elseif ($matches[$i][1] == "mixed") {
                // do nothing
            } elseif ($matches[$i][1] == "callback") {
                if (!is_callable($val)) {
                    trigger_error("$function(): Argument '" . $matches[$i][2] . "' should be a valid callback", E_USER_WARNING);
                    return false;
                }
            } elseif ($matches[$i][1] == "resource") {
                if (!is_resource($val)) {
                    trigger_error("$function(): Argument '" . $matches[$i][2] . "' is not a valid resource", E_USER_WARNING);
                    return false;
                }
            } elseif ($matches[$i][1] == "array") {
                if (!is_array($val)) {
                    trigger_error("$function(): Argument '" . $matches[$i][2] . "' must be an array, " . gettype($val) . " given", E_USER_ERROR);
                    return false;
                }
            } else { // class
                if (get_class($val) != $matches[$i][1]) {
                    trigger_error("$function(): Argument '" . $matches[$i][2] . "' must be an instance of " . $matches[$i][1] . ", " . (is_object($val) ? "instance of " . get_class($val) : gettype($val)) . " given", E_USER_ERROR);
                    return false;
                }
            }
        }
        return call_user_func_array(array($this, "_$name"), $arguments);
    }
}

class CallCheckTest extends CallCheck {
    /** Ukázka použití přetěžování pro napovídání skalárních typů
    * @param int $i testovací parametr
    * @return null výpis testovacího parametru s převedeným typem
    */
    protected function _check_params_test($i) {
        var_dump($i); // int(5)
    }
}
$a = new CallCheckTest;
$a->check_params_test("5");
?>

Všechny metody, u kterých se mají kontrolovat parametry, se uvodí podtržítkem, takže budou volané přetíženě. Metoda __call zkontroluje a převede parametry a skutečnou metodu zavolá. Samotné metody se dál už o nic nemusí starat.

Pokud je potřeba předat parametry referencí, lze to při zapnuté direktivě allow_call_time_pass_reference udělat při volání metody – $a->check_params_test(&$i). Pokud je direktiva vypnutá, lze zavolat funkčně shodné call_user_func_array(array($a, 'check_params_test'), array(&$i)), které ale na rozdíl od předchozího případu nevyvolá chybu.

Před uvedením metody __callStatic v PHP 5.3 nelze tento postup použít u statických metod. Výhoda tohoto řešení spočívá v tom, že by stejným způsobem šel kontrolovat i návratový typ. Vzhledem k tomu, že všechny metody musí být deklarované jako protected, měla by metoda __call kontrolovat i viditelnost, ideálně přes dokumentační komentář @access. Kvůli dokumentaci by také bylo vhodné uvést skutečné jméno metody, komentář @name je ale bohužel povolen pouze u proměnných.

Závěr

Osobně bych asi ani jeden způsob napovídání skalárních parametrů nepoužil, ale obzvlášť tento postup by mohl být dobrou součástí nějakého frameworku.

Jakub Vrána, Řešení problému, 21.12.2007, diskuse: 15 (nové: 0)

Diskuse

ivan_d:

Taky jsem s typovou kontrolou laškoval. Ale přináší to ospravedlnitelnou výhodu, když tady stejně není kontrola při kompilaci? Nestává se z toho jen málo užitečná byrokracie?

ikona Jakub Vrána OpenID:

Výhoda spočívá v tom, že uvnitř funkce se můžu spolehnout na to, že dostanu správné datové typy, a už to nemusím v každé funkci vždycky znovu kontrolovat.

ikona finc:

No, sice se musi uznat, ze jsi v PHP skutecne dobry, ale tento pripad mi prijde jeste horsi nez predchozi.
Kdyz to vezmeme do dusledku, tak u PHP je takova typova kontrola dobra pro vyvojare, nikoli pro beh samotny.

U predchoziho pripadu si dokazu predstavit, ze v pripade nesplneni vyhodim vyjimku, zde by samozrejme melo byt neco podobneho. Zde by slo neco podobneho, boolean hodnota neni vypovidajici tomu, ze jsem nesplnil typovou kontrolu. Ale to podtrzitko u metody a ta nutnost extendovat, no nevim nevim.

Ale pokud jsi na nic dalsiho neprisel ani ty, tak to uz vazne jiny zpusob nebude :)

Zajimalo by me, jestli nekdo z pritomnych (krome jakubovych napadu a obaleni vlastnimi objekty) ma nejaky dalsi napad, jak toto resit.

ikona dgx:

jak to řešit? jednoduše:

<?php

function repeat($multiplier, $s)
{
    if (!is_numeric($multiplier) || $multiplier < 1) {
        throw new InvalidArgumentException('Multiplies has to be number greater than 0');
    }

}
?>

Stručné, srozumitelné, generující vysvětlující výjimky a jdoucí za rámec typové kontroly. Geniální, co? :-)

(snažím se říct, že prostá řešení bývají obvykle ty správná)

ikona finc:

Myslim, ze je to presne to, cemu se chci vyhnout :)))

To uz radeji toto:
<?php
function foo(Integer $cislo, String $pozdrav) {
   if ($pozdrav->equals("ahoj PHP")) {
      echo "Tak co, funguju?";
   }
}
?>

ikona karf:

Super, a co dál?

<?php

$cislo
= $cislo->add($jineCislo);

?>

?

ikona finc:

Samozrejme, ze takovy objekt musi byt nemenny. To znamena, ze nebude obsahovat setter metodu pro zmenu cisla.

Je jasne, ze pri "pocitani", je treba bud prevest typ zpet na int nebo mit nekde vedle nejakou Math tridu, ktera obsahuje staticke metody pro pocitani s cislem.

Samozrejme, ze to, co jsem napsal je take pouze namet. Jiste se nalezne rada omezeni.

Jinak ta metoda add by mela zase vracet objekt Integer a nebo nevracet referenci (respektive u PHP hodnotu).
<?php
echo $cislo; // 1
$cislo->add(new Integer(1));
echo
$cislo; // 2
?>

ikona karf:

Já to myslel jako vtip :)

ikona Jakub Vrána OpenID:

Problém tohoto přístupu je v tom, že kontrolu musím volat v každé funkci znovu, mohu ji provést špatně (nekonzistentně) a mohu na ní zapomenout.

Jak jsem psal – já bych taky asi ani jeden z popsaných přístupů nepoužil, je to spíše teoretický rozbor, ale klasický přístup nepovažuji za principielně správný.

Funkce by zkrátka podle mě měla počítat a ne kontrolovat typ parametrů – to by mělo být řešeno bokem a konzistentně.

Je to stejné jako s ošetřováním chyb pomocí výjimek – výjimky jsou hezké v tom, že dovolí kódu prostě počítat a ne pořád ošetřovat nějaké možné chyby.

ikona Jakub Vrána OpenID:

Tady přeci vůbec nejde o to, jestli funkce vyhodí chybu a vrátí false nebo vyvolá výjimku. Samozřejmě to jde triviálně přepsat nebo překládat funkcí set_error_handler(). Jde o to, že jsem použil stejný postup jako PHP – v okolním kódu nemusíš rozlišovat, jestli k chybě došlo v PHP nebo v obálce.

ikona karf:

Tohle řešení bych neváhal nazvat zvráceným (to se jako bude při každém volání metody vytvářet Reflection a provádět ten šílenej regulární výraz? Uff). Mně přijde ale nešťastná už samotná myšlenka roubování typové kontroly do jazyka, který to přímo nepodporuje.

optik:

Vzhledem k tomu, jak bude reflections v kombinaci pretizenym volanim funkci pomale, tak si troufnu tvrdit, ze zadny framework to nepouzije, kdyz by se to volalo na kazdou funkci nebo metodu, tak vykon bude desivy, s debug_backtrace (minuly clanek) to je to same.

Myslím, že reflections, debug_backtrace ... patří do věcí jako phpdoc, phpunit, různé generátory kódu atd. Ale pro použití na web aplikace e kvůli výkonosti nehodi.

K té typové kontrole, pomalu se děsím chvíle, kdy půlka lidí bude v PHP psát jako v javě/C++ a půlka pojede stylem jako do ted.

Michal Aichinger:

Parsovani dokumentace se mi zda opravdu moc nepekne. Reflection bych pouzil na spravne zjisteni viditelnosti metody

$method->isProtected()

ale ne ohledne zjistovani typu. PHP je netypovany jazyk a dost casto se stava ze vstupni parametry muzou byt ruzne podivne, napr. vlastni objekt, nebo jen string s nazvem tridy, coz by se normalne resilo polymorfismem, nicmene to v PHP nejde.

Pokud chce nekdo typovou kontrolu zakladnich typu pri deklaraci tridy, asi by mel radsi vyzkouset rozsireni: http://pecl.php.net/package/SPL_Types

No a na konec bys mel spis poradit jak na nejvetsi mor objektovych kontrol a to null a jak se mu vyhnout obloukem.

ikona finc:

Samotna reflexe je vyborny nastroj (pokud je v dobrych rukou), muze ti usnadnit spoustu veci, viz. ruzne ORM frameworky, kde mapovani muzes resit pres komentare. Skoda jen, ze je to v PHP tak pomale a neexistuje neco jako object.class.getAnnotations() :)))

Polymofrismus sice v PHP nelze, ale da se to resit poctem parametru metody, tutiz lze tak nejak urcit o co vlastne jde.

Kontrolu null lze obejit pomoci vzoru: Null Object.

ikona v6ak:

Nápad: co kdybys napsal skript, který na základě PhpDoc doplní příkazy pro type hinting?

Vložit komentář

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2018 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.