Odstranění PHP typu resource

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

PHP typ resource vznikl v PHP 3 jako náhrada tehdy neexistujících objektů. Programátor ho nemůže nijak vytvořit, pouze ho vracejí některé funkce. Stejně tak se nedá použít nijak jinak, než že se předá nějaké funkci.

Mnohem přirozenější je místo prostředků používat objekty. Funkce vracející prostředky budou konstruktory těchto objektů, funkce manipulující s prostředky potom jejich metody.

Vytvořil jsem proto projekt php-resource, který prostředky v PHP nahrazuje za třídy. Použití je přirozené:

<?php
$mysql = MySQL::connect();
$result = $mysql->query("SELECT 1");
while ($row = $result->fetchAssoc()) {
    echo $row[1];
}
?>

Základní třída, ze které jsou všechny varianty prostředků odvozeny, není složitá, ale používá late static binding, takže pro svou práci potřebuje PHP 5.3 a vyšší. Zdrojový kód této třídy je k dispozici v projektu.

Vytváření potomků z této základní třídy je triviální. Např. třída Image obalující práci s obrázky vypadá takhle:

<?php
class Image extends Resource {
    protected static $prefix = 'image';
    protected $destructor = 'destroy';
    protected $resources = array(
        // 'gd font' uses int
        'gd PS font' => 'psFreeFont',
        'gd PS encoding' => '',
    );
}
?>

A to je ještě jedna z těch složitějších, protože umí pracovat s více typy prostředků.

Serializace a export

Prostředky jsou jediný datový typ, který nelze serializovat (což se používá hlavně pro ukládání proměnných do session) a exportovat (což se používá hlavně pro kešování). Objektová obálka to umožňuje, takže objekty třídy Resource lze jak serializovat, tak exportovat. Při deserializaci a importu se prostředek defaultně vytvoří pomocí konstruktoru, kterému se předají stejné parametry jako při prvním zavolání, lze ale přidat i zavolání dalších funkcí:

<?php
$mysql = MySQL::connect();
$mysql->initializing = true;
$mysql->selectDb("test");
$mysql->setCharset("utf8");
$mysql->initializing = false;
// při deserializaci a importu se zavolají všechny tři metody
?>

Tohle je myslím skutečná přidaná hodnota, která jde nad rámec roztomilé objektové obálky.

Konstanty

Bylo by hezké, kdyby měly třídy vytvořené konstanty vztahující se k danému prostředku, aby se místo $result->fetchArray(MYSQL_BOTH) dalo volat $result->fetchArray(MYSQL::BOTH). PHP ale nedovoluje vytvářet třídní konstanty dynamicky, takže by zdrojový kód musel obsahovat dlouhé seznamy konstant, které bych navíc musel při vydání každé nové verze PHP aktualizovat. Přepsání konstant by do projektu sice dobře zapadalo, ale pravda je taková, že s odstraněním prostředků vlastně nemá nic společného.

Stejně tak by se dala vytvořit třída třeba pro pole, aby se místo array_rand($array) dalo volat $array->rand(), opět to ale jde nad rámec odstranění prostředků.

Parametry předávané referencí

Magická metoda __callStatic ani funkce func_get_args, které třída používá, nepodporují předávání parametrů referencí. Proto bylo potřeba funkce přijímající parametry referencí napsat ručně. Přemýšlel jsem i o tom, že by se tyto funkce automaticky generovaly, to by ale bylo poměrně pracné, navíc ani není kde vzít jejich seznam. K získání seznamu by se dala použít reflexe, to bych ale musel zprovoznit všechny extenze, proto jsem seznam funkcí nakonec vytáhl z dokumentace.

Narazil jsem také na jednu funkci, ke které se objektová obálka napsat nedá. Je to funkce fscanf, která přijímá libovolný počet parametrů předávaný referencí. Přepsal jsem ji nakonec takto:

<?php
function scanF($format, &$arg1 = null, &$arg2 = null, &$arg3 = null, &$arg4 = null, &$arg5 = null, &$arg6 = null, &$arg7 = null, &$arg8 = null, &$arg9 = null, &$arg10 = null) {
    if (func_num_args() > 11) {
        trigger_error('File::scanF() supports up to 10 variables', E_USER_WARNING);
    }
    $args = array($this->resource, $format);
    $count = min(11, func_num_args());
    for ($i=1; $i < $count; $i++) {
        $args[] = &${"arg$i"};
    }
    return call_user_func_array('fscanf', $args);
}
?>

Je to asi nejodpornější metoda, jakou jsem kdy napsal, ale opravdu jsem nepřišel na způsob, jak ji napsat obecně. Dlouhý seznam by se dal nahradit funkcí eval, to by ale bylo vytloukání klínu klínem. Metoda využívá toho, že funkce call_user_func_array s parametry předávanými referencí pracovat umí.

Závěr

Ve skutečnosti jsem si tenhle projekt dělal jen tak pro radost, abych si dokázal, že se prostředků v PHP dá celkem jednoduše zbavit. Hezčí by samozřejmě bylo, kdyby se prostředky vyhodily rovnou ze zdrojového kódu PHP, to ale podle mě nemá šanci na úspěch ani v PHP 6. Nicméně pokud je někomu uhlazená objektová syntaxe milejší než krkolomné předávání jinak nepoužitelných proměnných, tak může knihovnu zkusit použít.

Jakub Vrána, Ze zákulisí, 2.7.2009, diskuse: 15 (nové: 0)

Diskuse

ikona david@grudl.com:

Obálka nad fscanf (máš v článku sscanf) by se dala trošku upravit, ale omezení na fixní počet parametrů asi ne.

<?php
function scanF($format, &$arg1 = NULL, &$arg2 = NULL, &$arg3 = NULL, &$arg4 = NULL, &$arg5 = NULL, &$arg6 = NULL, &$arg7 = NULL, &$arg8 = NULL, &$arg9 = NULL, &$arg10 = NULL) {
    $args = array($this->resource, $format);
    for ($i = func_num_args() - 1; $i > 0; $i--) $args[$i + 1] = & ${"arg$i"};
    return call_user_func_array('fscanf', $args);
}
?>

Jinak dokumentaci se v tomto směru nedá 100% věřit. Napadá mě třeba fce extract, která oproti dokumentaci přijímá parametr (od nějaké verze) referencí; vím, mám takové věci hlásit, ale člověk do té bugs.php.net leze jen velmi nerad ;)

ikona Jakub Vrána OpenID:

Chytrá hlavička! V článku i v kódu jsem to opravil, děkuji. Myslel jsem, že s referencemi nedokáže pracovat ani call_user_func_array().

Víš přece, že chyby v dokumentaci můžeš hlásit přímo mě a já je obratem opravím (to platí pro všechny). Chyby v dokumentaci reportované na bugs.php.net opravuji tak jednou za rok a pokud vím, tak to nikdo jiný moc nedělá.

Nicméně to s tím extract() se mi nedaří reprodukovat. <?php extract(array("a" => 5)); ?> mi normálně funguje.

ikona david@grudl.com:

Tyjo asi nepotřebuje...

Pátral jsem v paměti, jestli jsem fakt tak blbej a tuším to začalo házet varování v 5.3.0 - předělával jsem kvůli tomu jednu funkci v Nette - ale ve final je to bez problémů. Na druhou stranu, připadalo mi logické, že kvůli flagu EXTR_REFS bude referenci potřebovat.

David grudl:

Tak fakt v dokumentaci bug je! http://forum.nettephp.com/cs/2034-chyba-v-limitedscope?pid=13553

ikona Jakub Vrána OpenID:

A v jaké verzi PHP se to projevuje? PHP 5.2.10 ani 5.3.0 mi žádnou strict ani jinou chybu nehlásí.

ikona Techi:

Můžeš uvést nějaký příklad z praxe, kdy potřebuješ serializovat/exportovat resource?

ikona Jakub Vrána OpenID:

Osobně to nepotřebuji. Jde spíš o to, že můžu do session/cache ukládat libovolné hodnoty a netrápit se tím, že tam něco uložit jde a něco ne.

papundeklová paní:

Mně to přijde jako:
* posedlost objekty a OOP
* kilobajty kódu navíc
* potenciální zavlečení překlepů a jiných chyb
* další zatížení paměti
* zesložiťování celého projektu
* odklon od KISS
* naprosté nepochopení toho, k čemu objekty slouží, na co se mají používat a jaké mají výhody

ikona v6ak:

"* posedlost objekty a OOP"
Bez komentáře.
"* kilobajty kódu navíc"
mysql_query($dbh, ...);
vs.
$dbh->query(...);
"* potenciální zavlečení překlepů a jiných chyb"
Kde?
"* další zatížení paměti"
IMHO nic významného.
"* zesložiťování celého projektu"
Kde?
"* odklon od KISS"
Co je jednodušší a intuitivnější?
... function ...(){
  ...
  $res = $this->dbh->query(...);
  ...
  // nechť mi to garbage collector uklidí $res
}
vs.
... function ...(){
  ...
  $res = mysql_query($this->dbh, ...);
  ...
  mysql_free_result($res);
}

"* naprosté nepochopení toho, k čemu objekty slouží, na co se mají používat a jaké mají výhody"
To, myslím, chápu, takže tento bod kritiky taky nechápu.

ikona Jakub Vrána OpenID:

Se všemi body v zásadě souhlasím, jde vlastně jen o takový Proof-of-Concept – jak bych chtěl, aby se chovalo přímo PHP, ale realizovat to ve vlastní vrstvě považuji taky za nepřirozené.

Nesouhlasím jen s posledním bodem, protože typ resource vlastně přesně splňuje definici objektu (má nějaké vlastnosti a metody, které s ním pracují), takže nahradit ho skutečným objektem je zcela logické. Díky tomu se dají využít i všechny výhody objektů, např. dědičnost.

pojízdná kočka:


vs6ak:
kilobajty kódu navíc a možnost zavlečení chyb se samozřejmě týká zdrojových souborů, které by při tomto přístupu musely být vytvořeny. ostatní, co namítáš, by se taky dalo lehce vyvrátit, ale nějak na tebe nemám náladu, ukazovat ti, kde konkrétně se mýlíš ;-)

Jakub:
inu, u posledního bodu jsem se mohla trochu unáhlit - některé z těch vlastností by se daly uznat, ale to takové sporné. Upravila bych to, že bych ty hlavní vlastnosti objektů rozepsala detailněji takto:
* zapouzdření - to by se dalo uznat, i když konvence názvosloví procedur mysql_xxxx() (alespoň opticky, při pohledu na zdrojový kód) supluje přibližně to samé. Objekty ale vždycky budou kompromis, právě díky tomu, že konstanty takto řešené nemají (dokonce bych řekla, že na základě právě tohoto faktu jako vítěz vychází procedurální přístup).
* dědičnost - pokud by nešlo o typ resource (který - jak lze vyčíst z titulku - byl předmětem tohoto článku) a šlo by pouze o koncept nahrazení procedurálního zápisu objektovým, pak u této výhody u objektů není co nahrazovat -  neboť procedury v PHP nic nedědí a ani nemohou. Jistě, nic nikomu nebrání vytvořit si další třídy, které budou dědit vlastnosti těch "obalených", ale to už nota bene bude znamenat odklonění od tohoto konceptu a především - co z toho? ;-) Opět mi to přijde jako takové plácání objektových báboviček na pískovišti.
* polymorfismus - v tom jsi mě dokonale zmátl ;-) A to tím, že jsi instanci nazval $mysql. Pokud bych dělala nějaký projekt (s tímto konceptem), používala bych MySQL a měla bych najednou projekt převést třeba do PostgreSQL, tak by po dokončení tohoto úkonu použití instance $mysql (které může být na 1000+1 místech v X souborech projektu) působilo poněkud zmatečně (to je trochu slabé slovo - úplně by to zruinovalo přehlednost celého zdrojového kódu). Takže bych se přimlouvala na nějaký obecný název (který by reflektoval, že jde o připojení k databázovému stroji) - a pak by už to vypadalo lépe. -Tak, to by bylo obecně - a konkrétně? konkrétně pro MySQL a PostgreSQL (nebo jakoukoli jinou databázi) budou vždy nějaké odlišnosti (specifická syntaxe MySQL jako např. CALC_FOUND_ROWS, specifické proměnné, nastavení, ... - versus něco, co mají zvláštního zase jiné databáze), takže si pouhou změnou instance objektu nepomůžeme a stejně budeme muset procházet celý zdroják a hledat použití těchto specifických funkcí. Suma sumárum - tomu, kdo se do tohoto konceptu pustí, brzy nadšení a úsměv na tváři opadne.

Aby to nebylo celé tak destruktivní - uznávám, že toto tebou popsané řešení problém uložení typu resource řeší (čímž jsem se doufám vrátila zpátky k tématu, o kterém by tato diskuze měla být ;-)).

ikona v6ak:

No možná to zatížení *v případě použítí tohoto PoC* bude větší, i když ne kritické. V případě nativního řešení by to mělo být prakticky zanedbatelné.

"Objekty ale vždycky budou kompromis, právě díky tomu, že konstanty takto řešené nemají"
Pokud mluvíš o tomto PoC, pak ano. Jinak není problém udělat konstantu jako třeba PDO::ATTR_ERRMODE.

S dědičností celkem souhlasím. Pokud by se to ale začlenilo do PHP, pak by to mělo být OK. Stejně tak s polymorfismem.

"ostatní, co namítáš, by se taky dalo lehce vyvrátit"
Tak mi PLS vyvrať aspoň to KISS.

ikona Jakub Vrána OpenID:

Já jsem se nesnažil napsat nějakou univerzální obálku nad např. databázovými extenzemi a nechápu, jak tě to napadlo. Jde prostě o náhradu typu resource objekty. Pokud by si někdo chtěl napsat univerzální obálku, tak se mu to možná bude dělat nad takovýmto objektovým rozšířením lépe a možná taky ne, ale mně o to každopádně nešlo.

Konstanty by se daly zapouzdřit také, jen by to byla otravná práce, která by vyžadovala úpravu knihovny při přidání každé nové konstanty. Proto jsem se do toho nepouštěl.

Tomáš J.:

Vím, že píši po 5 letech, ale chci se zeptat, zdali se dá tato třída použít v případě, že chci se serializovaným resourcem nadále pracovat po znovunačtení stránky?
Nevím, jestli jsem to pochopil správně. Napíšu přímo k věci.

Potřebuji se připojit k FTP serveru a vrácený resource uchovávat v session. Chci se tak vyhnout tomu, abych se při každém načtení stránky musel připojovat k FTP (je to dost pomalé). Dá se to použitím této třídy?

ikona Jakub Vrána OpenID:

Ne, tato třída s tím nijak nepomůže. Dalo by se to vyřešit tak, že bych měl dlouhoběžící skript, který by si připojení uchovával, a komunikoval s ním. To je bohužel dost krkolomné.

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.