Kdy použít isset() a kdy NULL?
Školení, která pořádám
Na jazykové konstrukci isset mi nevyhovuje jedna věc: nerozlišuje, jestli je testovaná proměnná nenastavena nebo jestli je nastavena, ale obsahuje hodnotu null. Často se proto používá v druhém významu (a používal jsem ji tak dosud obvykle i já). Čemu to vadí?
<?php
// o tomto překlepu se nijak nedozvíme
if (isset($application["autor_id"])) {
}
// způsobí chybu úrovně E_NOTICE
if ($application["autor_id"] !== null) {
}
?>
Pokud víme, že proměnná bude existovat, a pouze chceme zjistit, zda neobsahuje hodnotu null, je bezpečnější to zjistit porovnáním (včetně kontroly typu) s touto hodnotou nebo funkcí is_null.
Co když nevíme, jestli proměnná bude existovat (třeba proto, že jsme ji dostali od uživatele), a chceme to zjistit? Mějme např. pole $row
se všemi platnými sloupci v klíčích, ze kterého chceme vypsat hodnotu sloupce, který dostaneme od uživatele:
<?php
// neprojde pro sloupce obsahující hodnotu NULL
if (isset($row[$column])) {
}
// projde pro všechny platné sloupce
if (array_key_exists($column, $row)) {
}
?>
I v tomto případě je tedy lepší se bez isset obejít.
Konstrukci isset($row[$column])
je tedy dobré chápat jako zkratku za array_key_exists($column, $row) && $row[$column] !== null
. To se taky může někdy hodit, ale většinou je lepší z této podmínky využít jen jednu část.
Poznámka: U běžných proměnných (a nikoliv u klíčů pole) konstrukci isset prakticky nepoužívám, protože tyto proměnné explicitně inicializuji.
Objekty
U objektů můžeme chování konstrukce isset upravit pomocí metody __isset. To se hodí i proto, že nelze přetížit chování funkce property_exists (obdoba array_key_exists). Nešlo by tedy rozumně zjistit, jestli virtuální vlastnost skutečně neexistuje nebo jestli existuje a má jen hodnotu null. Chování můžeme upravit tak, že true vrátíme pro všechny existující virtuální vlastnosti:
<?php
class ReadOnly {
protected $readOnly = array("set" => 1, "null" => null);
function __isset($name) {
return array_key_exists($name, $this->readOnly);
// return isset($this->readOnly[$name]);
}
function __get($name) {
return $this->readOnly[$name];
}
}
?>
Tato úprava chování ale bohužel vede k nepříjemným paradoxům:
<?php
if (isset($readOnly->null)) {
$null = $readOnly->null;
var_dump(isset($null)); // false
}
?>
Obvykle je tedy lepší v metodě __isset zachovat běžné chování a pro zjištění samotné existence zavést speciální metodu.
Totéž se týká i metody offsetExists.
Přijďte si o tomto tématu popovídat na školení Programování v PHP 5.
Diskuse
wdolek:
__get, __set, __isset, __call a dalsi metody jsou tzv "magicke". nerozumim tomu, proc se tedy v tak hojne mire vyskytuji v nekterych ceskych projektech, pricemz by kod urcite slo napsat cisteji a bez "magie".
kdzyz uz se zde rozebiraji zaklady PHP, bylo by hezke, kdyby se mladi a nezkuseni programatori fackovali za nadmerne (zne|po)uzivani magickych metod, a ucili se psat nejak lidsky.
chapu, ze ne vzdy se to da obejit, a jsou situace, kdy se pomoci __call nahrazuje pekne vymysleny overloading treba v Jave. posledni dobou ale magii vidim cim dal tim vic, a to mne ponekud znepokojuje (protoze, kdo pak ma cist zdrojaky a zjistovat, co objekt dela, kdyz je vse krasne "obfuskovane" a namotane v __magic metodach).
Magické metody jsou prostředkem k tvorbě expresivnějších vyjadřovacích postupů, které můžou při jejich pochopení vést k pohodlnější tvorbě kódu. Např.:
<?php
foreach ($this->db->application() as $application) {
echo "$application[title] ($application[web]) - $application[slogan]\n";
echo $application->author["name"] . "\n";
}
// versus
foreach ($this->getDb()->getTable("aplication")->getRows() as $application) {
echo $application->getColumn("title") . " (" . $application->getColumn("web") . ") - " . $application->getColumn("slogan") . "\n";
echo $application->getRelatedRow("author")->getColumn("name") . "\n";
}
?>
První postup vyžaduje dodatečné znalosti, bez kterých může být obtížněji pochopitelný. Pokud ale tyto znalosti uživatel získá, tak se kód vytváří mnohem pohodlněji.
Ze stejného důvodu vznikly třeba i šablony – ty se sice dají zapsat přímo v PHP, ale je to tak nepohodlné, že je tady prostor pro nové jazyky. Např. pro pochopení zápisu <span title="{$title}"> se člověk taky musí něco naučit, ale jakmile to udělá, tak zápis ocení ve srovnání s <span title="<?php echo htmlspecialchars($title); ?>">.
wdolek:
ano, to ale neplati pri omezenem vyctu moznosti ($db->select, ->insert, ->delete ...)
ostatne, pokud chci mit neco jako "property" objektu, tak to nadefinuji jako property objektu ;D a nestrkam to pres __set a neziskavam pres __get.
chapu, ze to muze byt hezke u nejakeho service locatoru :), obecne bych se ale "magie" vyvaroval, a spise od toho odrazoval.
(a dalsim duvodem muze byt uz treba jen to, ze PHP IDE si proste s magickymi vlastnostmi/metodami neporadi ;))
Běžné property objektu jsou omezené v tom, že je musím naplnit předem, takže se nedá použít lazy-loading. Což je třeba u $application->author zcela zásadní věc, protože takovýchhle vazeb může být v tabulce spousta a já chci rozhodně načíst jen ty, ke kterým doopravdy přistoupím.
Chtěl jsem ukázat především jednu věc: magické metody považuji za takový mezistupeň k vytvoření nového jazyka uzpůsobeného cílovému problému. To se může zdát jako poměrně radikální krok, ale třeba u šablon se to ujalo. Jsou s tím samozřejmě spojené jisté komplikace (člověk se musí něco naučit), ale pokud je výsledek pro řešení problému skutečně pohodlnější, tak to lenoch ocení.
IDE neporadí – to jako že nedokážou napovědět jejich název? To ale nedokážou tak jako tak: $application-> – odtud už mi IDE nic nenapoví VS $application->getTable(" – odtud už mi IDE nic nenapoví.
IDE může krásně napovídat, když budeme mít třídy a jejich vlastnosti (které budou zhruba odpovídat tabulkám a sloupcům v databázi).
Ten tvůj přístup znamená, že nemusíme udržovat datový model ve formě tříd v PHP a máme jen databázi a pak PHP kód, který ji používá (a v něm už názvy tabulek a sloupců zadrátované jsou). Ono to je na jednu stranu lákavé a skutečně to část práce ušetří (nemusíme synchronizovat model v databázi a v programovacím jazyce), ale na druhou stranu je to dost nebezpečný přístup a taková aplikace je poměrně „křehká“.
BTW: koukám, že používáš „304 Not Modified“ i pro celé stránky (ne jen obrázky, js, css atd.), což už se dnes moc nevidí – mimo jiné to často vede k chybám – tady se mi nezobrazil můj komentář, který jsem před chvílí vložil, už jsem myslel, že se ztratil, a objevil se až po vynuceném načtení stránky (Ctrl+Shift+R). Ale při správné práci s hlavičkou „Last-Modified“ by to mělo fungovat… Možná je chyba na mém přijímači (Firefox 5.0), ale spíš bych tipoval, že to bude na serveru.
Kód na serveru je v pořádku – posílá tyto hlavičky:
Last-Modified: Mon, 01 Aug 2011 12:12:35 GMT
Cache-Control: must-revalidate
Vložení nového příspěvku čas modifikace pochopitelně aktualizuje.
Ve Firefoxu 5 mi to bez problémů funguje, problém bude tedy spíš u tebe.
P.S. Opravdu netuším, jak tvá reakce souvisí s původním příspěvkem.
Teď už se mi tu chybu nepodařilo zopakovat. Ale předtím se to opravdu stalo, nevymyslel jsem si to :-) Klikl jsem na logo webu a dostal se na úvodní stránku, tam byl ještě původní počet komentářů (o jeden méně, než mělo být) a pak jsem klikl na článek a komentář tam chyběl – objevil se až po Ctrl+Shift+R.
Nechci s tím prudit a pokud ti to nevadí, tak to ignoruj :-) je to jen detail. Ale zase se mi to stalo – minimálně počítadlo nepřečtených komentářů nefunguje správě, na titulní stránce se mi u posledního zápisku zobrazovalo 9 komentářů a z toho 1 nepřečtený – přitom jsem ho už četl – po vynuceném obnovení titulní stránky se mi už zobrazuje 9/0.
To je pravda, přečtení komentářů čas modifikace nezmění.
Než ručně udržovat synchronizovanou databázi a třídy PHP, to si radši napíšu do IDE plugin, který bude schopen napovídat i na základě obsahu databáze.
Čím by aplikace podle tebe měla být křehká? Že se v kódu můžu odkázat na tabulku, která neexistuje? To se ale při synchronizaci databáze a kódu může stát taky – stačí, když z databáze smažu (nebo přejmenuji) nějakou tabulku, neupravím kód a jsem obětí stejného problému.
Ke stejnému problému ostatně může dojít i v případě, že kód upravím – o chybě se dozvím stejně až při spuštění, tedy přesně ve stejný okamžik jako u elegantního přístupu.
Když už chce člověk „prasit“ a mít rychle hotovo, přijde mi rozumnější přístup mít jako primární model třídy v daném programovacím jazyce a databázi generovat/aktualizovat podle něj (na to jsou samozřejmě nástroje, není potřeba to dělat ručně).
Křehká je ta aplikace v tom, že jedna změna (v databázi) se projeví na spoustě jiných míst (v kódu) a tento dopad je poměrně těžko uchopitelný. Když budeme mít aplikaci „pojištěnou“ formou těch tříd, změny je sice potřeba taky udělat na všech místech, ale snadno dohledáme, kde. Např. když se změní název sloupečku nebo datový typ nebo vazba, změníme danou třídu představující model a kód se nám na spoustě míst „rozbije“ – pak víme, co je potřeba opravit (což je rozdíl oproti volání nějakých dynamických metod nebo přistupování k vlastnostem objektů, které možná existují a možná taky ne – což se rozhodne až v době běhu).
Je to trochu podobný rozdíl, jako když budu mít [1] na spoustě míst v kódu rozeseté textové řetězce nebo čísla versus [2] budu používat konstanty a všude se na ně odkazovat. V obou případech program funguje stejně dobře, ale změny se lépe provádějí v tom druhém a nemusím se při nich tolik „bát“.
A k tomu: „napíšu do IDE plugin“
Tohle je dost často právě ta hranice a měřítko, kterou cestou se vydat. Napsat si totiž člověk může cokoli, ale otázka je, jestli to dělat musí, nebo zda se není lepší poohlédnout jinde, kde to má bez práce. Navíc ona to není volba: „buď *ručně* udržovat synchronizované… NEBO si napsat plugin“.
Odpusť si prosím termíny jako „prasit“. Psát krátký a elegantní kód za prasení totiž nepovažuji. Mně se naopak jako prasení zdá ručně psát nebo generovat mraky balastního kódu, který nenese absolutně žádnou myšlenku a v aplikaci je jen proto, že někdo neměl dost invence na to, aby vymyslel, jak se bez něj obejít.
Oba víme, že ty generovací nástroje nejsou úplně dokonalé a bez ruční údržby databáze se často neobejdeme. Alespoň to říkal Honza Tichý :-).
Jak si představuješ to „snadné dohledání“? Já bych si představoval, že kód neprojde kompilací. Tak tomu ale v PHP bohužel není, takže skončíme až na běhové chybě – přesně stejně jako při přímé komunikaci s databází.
Tvůj příměr je nepřesný. Když mám v kódu textové řetězce, tak hrozí, že se o chybě vůbec nedozvím. To u popisovaného příkladu ale neplatí.
Spouštět jakýsi generovací nástroj taky považuji za ruční práci. A i když bych to celé měl navázané na jednu klávesovou zkratku, tak je to pořád něco, na co musím myslet a co musím udělat.
To „prasit“ jsem dal schválně do uvozovek, neber to jako něco vyloženě negativního, je to jiný styl vývoje než taková ta precizní „hodinářská“ práce, kdy se člověk snaží o dokonalost – což ale někdy není potřeba a program splní svůj účel i bez toho (a naopak ta precizní práce by byla neekonomická).
Nevím, co říkal Honza Tichý, nesleduji ho. Ale co se týče generování obecně: musí fungovat dobře bez ručních zásahů – vygenerovat si kód (nebo databázi) a pak v tom dělat ručně změny je cesta do pekel. Musí to být automatizovaný opakovatelný proces. Na takovém generování pak není nic špatného, je to jako když si člověk vyrobí z SVG nebo XCF obrázek v PNG, nebo zkompiluje program, úplně normální věc.
Nejprve si ujasněme, co kdo spatřuje pod pojmem dokonalost. Např. na následujícím kódu:
<?php
class Product {
private $id;
private $name;
public function getId() {
return $this->id;
}
public function getName() {
return $this->name;
}
public function setId($value) {
$this->id = $value;
}
public function setName($value) {
$this->name = $value;
}
}
?>
Tohle já totiž považuji za zoufalý přístup se spoustou opakovaného kódu, který je nepřehledný, náchylný k chybám a náročný na úpravu. A spíše než „hodinářská“ práce to je ta nejhorší nádeničina.
Teď konkrétněji – jaký používáš generátor, že funguje dobře i bez ručních zásahů? Jak už jsem říkal – i tak to považuji za otravu, protože generátor musím ručně spouštět.
Můžeš to klidně napsat v Javě takhle*:
<?php
public class Product {
@Getter @Setter private int id;
@Getter @Setter private String name;
}
?>
Případně to jde v C# elegantně vyřešit pomocí vlastností (to je asi jediná zajímavá věc na tomto jazyce).
Co se týče té synchronizace databáze a tříd – rád si datový model navrhnu sám ručně a pak k němu dopíšu ty třídy a mapování. Je to dané tím, že databázi beru jako něco hodně důležitého, trvalého, zatímco aplikace je pomíjivá, časem se může vyhodit a napsat nová, nebo může pracovat víc aplikací nad jednou databází – takže mám tendenci začínat od modelu v databázi… Ale mám tu i jeden systém, kde jsme to dělali přesně naopak – primární je aplikace a datový model je definovaný v ní pomocí javovských tříd a jejich anotací a objekty v databázi se automaticky generují/aktualizují podle nich. Měl jsem k tomu zpočátku nedůvěru, ale funguje to až překvapivě dobře. Nástroj: JPA/Hibernate.
Možný je i opačný přístup – primární model mít v DB a třídy generovat z něj. Je to trochu složitější, ale taky to funguje. Ať tak nebo tak, ty entitní třídy neberu jako nějaký opruz a balast navíc, ale jako velice užitečnou pomoc.
*) není to v PHP, ale kód v jiném jazyce tu zřejmě nejde zadat.
Stejně jako ty považuji za důležitou především tu databázi, takže taky začínám od ní. Víš o nějakém nástroji (nejlépe pro PHP), který ti z databáze vygeneruje kód, na který už se nemusí sahat a který lze kdykoliv přegenerovat?
Zkus prosím specifikovat tu užitečnou pomoc, kterou ti entitní třídy dávají.
IDE může napovídat i bez existence proměnných, třeba pomocí anotací @property.
Ale obecně tohle není moc dobrý argument, protože mapování 1:1 mezi class diagramem a relačním schématem, byť nesmírně populární a hype nedávných let, je mnohými považováno za antipattern a tedy jde o přinejmenším diskutabilní techniku. Nevyřeší cokoliv složitějšího než SELECT * FROM table WHERE ...
Proto píšu „zhruba“… Ale o to (zda třídy pasují na databázi 1:1 nebo nějak jinak) vůbec nejde – jde o to, zda máme v tom kódu/IDE k dispozici ten datový model (ať už jakoukoli formou) nějak na jednom místě – nebo zda ho pouze používáme, aniž bychom ho nějak předem deklarovali.
Mám radši statické a silné typování a radši si ten model nějak popíšu a až pak s ním budu pracovat, než abych to dělal bez toho (takový program je pak trochu „na vodě“). Ale uznávám, že i ten dynamický přístup má něco do sebe, dají se v tom dělat různá kouzla a pro menší programy nebo na různé „hraní si“ je to zajímavé.
Schéma je obvykle popsané v databázi, to je autoritativní zdroj této informace. Jakákoliv duplikace této informace (ať už ruční nebo ručně generovaná) je porušení DRY, navíc může vést ke vzniku nekonzistencí.
A kód typu:
<?php
echo $application->author["name"] . "\n";
?>
nemůže vést k nekonsistenci? (tady tište předpokládáme, že v datovém modelu je „aplikace“, která má „autora“, a ten má nějaké „jméno“ – což nemusí být pravda a kód není konsistentní s databází)
K nekonzistenci to vést nemůže, protože nekonzistence by způsobila chybu. Nekonzistencí mám na mysli např. to, že se sloupec v databázi jmenuje visits a v aplikaci mu říkáme visitors (třeba z historických důvodů). To je při mapování možné a teoreticky to ušetří práci (protože když se v databázi přejmenuje sloupec, tak to v aplikaci stačí říct na jednom místě), ale vede to právě k té nekonzistenci, která způsobuje zmatky.
Přijde mi, že pokud se bavíme o PHP, pak z velké části jde jen o dvě cesty vedoucí k prakticky stejnému cíli:
* Chci-li mít napovídání, potřebuju nějakou synchronizaci kódu s definicí (jedním směrem - teď neřeším kterým) nebo plugin do editoru.
* V PHP statickou typovou kontrolu stejně nemám.
* Synchronizace nemusí být něco 'na klávesovou zkratku', můhže se provádět i naprosto automaticky.
Pokud se přesuneme ke staticky typovaným jazykům, jako je Java a Scala, přístup z NotORM už začne být nevýhodný. (Ano, dovedu si představit port NotORM do Scaly s prakticky nezměněnou syntaxí...) Ten nám zde totiž neposkytne statickou kontrolu kôdu na takové úrovni, jako jiné knihovny (např. Squeryl).
Samozřejmě, řeč nemusí být jen o NotORM, jde o jeho dynamické ampování.
Jak by fungovala ta naprosto automatická synchronizace?
Jedna z možností je, že se bude nějak detekovat změna v DB (plugin do DB, plugin pro administrační nástroj DB, polling apod.) a v případě změny se příslušná část / příslušné části přegenerují. On to vlastně nemusí být až tak plugin do DB - u lokální DB by stačilo sledovat nějakým observerem (na Linuxu INOTIFY) změny souborů, které obsahují definice tabulek. Možná by takto šlo vytvořit podporu pro mnoho DB enginů celkem jednoduše.
> nerozumim tomu, proc se tedy v tak hojne mire vyskytuji v nekterych ceskych projektech, pricemz by kod urcite slo napsat cisteji a bez "magie"
Nesouhlasím s tím, že použití magických metod vede k magickému kódu, tím se totiž myslí spíš nezdokumentované a neočekávané chování, což může nastat i bez použití magických metod a naopak. Magické metody jsou vlastně jen "implementační detail".
Za sebe bych chtěl říct, že naopak chytře použité magické metody vítám. PHP je dynamický jazyk a nevidím tedy důvodu, proč nevyužívat jeho dynamické vlastnosti. Zvlášť pokud to může zjednodušit práci. Např. použití SOAP klienta díky __call() je oproti jiným jazykům úloha pro kojence :-)
<?php
$client = new SoapClient($wsdlUrl);
$response = $client->HelloWorld();
?>
David Grudl:
Nechtěl bys tam ješte Jakube doplnit, že totéž se týká offsetExists()?
Nouma:
array_key_exists je prý hodně pomalá funkce (full scan pole) ... isset by mělo být mnohem rychlejší (index).
Pokud tedy chci ošetřit výběr prvku pole na jeho neexistetci (zatim mam notice vypnutý, ale časem bych chtěl zapnout) tak by asi mělo být nejlepší zápis:
<?php
echo $Config['paticka'];
?>
nahradit zápisem
<?php
echo PrvekPole($Config,'paticka');
function PrvekPole($pole, $klic)
{
if (!isset($pole[$klic])) return null;
return $pole[$klic];
}
?>
Předpokládejme, že pole $Config je fakt velký a napč. klíč 'paticka' je nebo není zadán a funkce PrvekPole() se bude volat hodně krát ...
Souhlasíte se mnou ? nebo je nějaký lepší řešení?
PS: Nejradši bych měl zaplý E_NOTICE pro vše kromě tahání neexistujícího klíče z pole - nešlo by to nějakou legrací s použitim nějakýho vlastního error handleru ?
Jakub Vrána :
Tyhlety „prý“ si raději nejdřív ověř. Třeba tímhle kódem:
<?php
$ar = array_fill(1, 1e5, false);
$start = microtime(true);
for ($i=0; $i < 1e5; $i++) {
//~ isset($ar[50000]);
array_key_exists(50000, $ar);
}
echo (microtime(true) - $start) . "\n";
?>
Funkce array_key_exists() je pochopitelně asymptoticky stejně rychlá jako isset(). Rozdíl je jen v konstantním násobku, protože isset() je jazyková konstrukce a array_key_exists() normální funkce. Neexistuje důvod, proč by array_key_exists() měl dělat full scan, když stačí spočítat haš klíče a podívat se do haš tabulky přesně stejně jako to dělá isset().
Popletl sis to s in_array().
Vlastní error handler by napsat šel, ale musel by se rozhodovat jen na základě textové hlášky „Undefined offset“.
rmaslo:
No, že by array_key_exists dělal fullscan si tedy nemyslím, ale také mi vychází mnohem pomalejší než isset. Nakonec jsem našel proč. Souvisí to s
http://www.php.net/manual/en/features.gc.…-basics.php a docela hezky je to vysvětlený v
http://phpfashion.com/php-cerna-magie-optimalizace Obecně si to poměrně rozsáhlé pole totiž všude předávám odkazem (občas do něj něco přidám a chci tuto změnu všude).
Ale jak předat pole odkazem do array_key_exists ? To je docela problém ...
<?php
array_key_exists('neco', &$pole)
?>
naráží na "call-time pass-by-reference" is deprecated (5.3x) a v PHP 5.4 je zrušeno...
Zkoušel jsem array_key_exists('neco', &$pole) nahradit za call_user_func_array(array_key_exists, array('neco', &$pole)) což by prý mělo předávat referencí, ale rychlost (PHP 5.3.3) je bohužel stejná jako u předání hodnotou...
Nezná někdo nějakou fintu jak po zakázání "call-time pass-by-reference" zjistit existenci klíče pole (které je předáváno odkazem ) opravdu rychle? Tj. buď jak předat do array_key_exists to pole odkazem nebo nějakou jinou fintu.
v6ak :
Nestačilo by se vyhnout příznaku is_ref?
Jakub Vrána :
Napadlo mě jedno hloupé řešení a ani nevím, jak je rychlé: Prostě zkusit k prvku pole přistoupit pomocí $pole['neco'] a když to vyhodí E_NOTICE, tak tam prvek nebyl, jinak ano.
Martin:
Jen drobnost, našel jsem, že is_null(var) je 4x pomalejší než if(var === null)
Diskuse je zrušena z důvodu spamu.