Mirin webspace

Nejbohatší život má ten, kdo žije s minimem nároků

29. 12. 2010 - Komentáře (10) PHP

PHP drobky a novinka

Lidé, kteří s PHP začínají, dost často již něco v nějakém jiném jazyku vytvořili. Nejčastěji Perlu, C/C++, C#, Javě. A tak většinou projdou nějaké základy formou tutorialu a začnou rovnou řešit zadaný úkol. Takto s PHP začínali a začínají spousty lidí, řekl bych většina. Až postupem času, pokud člověk není líný, začne zjišťovat, že existují malé, ale docela důležité věci, kterých si na první pohled nevšiml a pomocí kterých lze určité věci udělat jinak a u těch bych se dneska zastavil. Určitě už se to objevilo, zejména asi u J. Vrány, který je na tohle specialista, ale nikdy to neuškodí zopakovat, případně přehodnotit určité postupy.

Každá funkce vrací implicitně null

Tohle je věc, kterou jsem si uvědomil dost pozdě. Když si to člověk uvědomí i s tím, že null se po přetypováním na bool změní na false, tak to dost věcí zjednoduší. Samozřejmě se to netýká jen funkcí, ale i metod.

tuto funkci
 
function makeTest() {
 ...
 if (!$foo) return null;
 ...
 ...
 return null;
}
 
stačí zapsat
 
function makeTest() {
 ...
 if (!$foo) return;
 ...
 ...
}

Schválně, kolikrát v kódu máte if (xxx) return null, nebo na konci funkce return null, případně jen prázdné return.

Také vlastně všechny PHPDoc komentáře v metodách by měly striktně vzato vypadat takto

/**
 * @param string $name
 * @return null
 */
public function setName($name) {
 $this->name = $name;
}

Citlivost na velikost písmen v PHP je peklo

Např. výše uvedená konstanta NULL je case insensitive, stejně jako FALSE, TRUE, já je třeba píši pokaždé s malými písmeny, naproti tomu když definujete konstantu přes funkci define(), tak je implicitně case sensitive, ale můžete ji použít i jako case insensitive s tím, že dostaneme E_NOTICE warning. Dokonce můžete define() vnutit definování konstanty jako case insensitive.

Že proměnné ($variable) jsou case sensitive a funkce/metody insensitive (htmlSpecialChars()), je známá věc, na kterou nadával asi každý tvůrce PHP frameworků už asi milionkrát. Málo už se ví, že názvy tříd jsou také case insensitive a společně s tím, že různé operační systémy se také chovají různě k citlivosti na velikost písmen v jménech souborů, je to už dost problém.

Završují to speciality jako array, echo, include, print, které nejsou skutečnými funkcemi, ale syntakticky se jako funkce zapisují a platí pro ně tedy také case insensitive.

Tahle věc mi tedy v PHP vadí mnohem více než zmatek v pojmenování built-in funkcí a jejich parametrů.

Využívejte slabého typování na maximum

Poměrně pozdě jsem také přišel na to, že slabé typování je, co se týče efektivity zápisu, čitelnosti a množství kódu jednoznačné plus. Zejména konverze na bool v podmínkách typu if, while je věc, která využije slabého typování na maximum.

Proto velmi doporučuji a také využívám konstrukce jako if ($var); if (!$var) kde to jen jde. Striktní operátory jako === v souvislosti s userspace kódem už používám jen velmi zřídka. Stejně tak jsem už v podstatě zapomněl na isset, empty, is_null. Pokud budete rozumně využívat slabého typování, tak to na tento stav jednoznačně vede.

Přiřazení je operátor jako každý jiný

Rovnítko (=) jako operátor přiřazení je další věc, kterou jsem objevil také dost pozdě. Trik je v tom, že výsledkem přiřazení je přiřazovaná hodnota a proto se může tento operátor využívat i tam, kde by to člověk na první pohled moc nepředpokládal.

$a = $b = ($c = 1) + 4;
 
if ($a = $this->getCount()) {}
while (($test = Test::getVal()) && $test > self::MAX) {}
foreach (($arr = getArr()) as $val) {}
 
$firstIndex = ($indexes = $this->getIndexes()) ? $indexes[0] : null;
 
foreach (($vals = $this->getData("
 select a,b
 from table
 where a<%i", $max)->fetchAll()) as $val) {
} 
 
if (!$a = $this->getCount()) {} - toto je už poměrně hodně kontroverzní

Dost často se použití přiřazení v podmínkách nedoporučuje zejména z důvodu podobnosti s operátorem ==. Nejsem příznivcem této zásady, naopak, proč se ochuzovat o výsledek operátoru přiřazení. Výše uvedené zápisy jsou pro mne také lépe a zejména rychleji čitelné než např.

$a = $this->getVar();
if ($a) {}

S přiřazením v podmínkách souvisí zejména to, že je potřeba dát si pozor na priority, operátor přiřazení má velmi nízkou prioritu a proto je potřeba občas závorkovat, viz. výše příklad s while. To je také důvod poměrně problematického zápisu if (!$a = getVal()), ačkoli funguje dle očekávání - do $a se přiřadí výsledek funkce a pokud tento není při konverzi na bool true, pak je podmínka splněna. Je proto možná lepší využít závorek if (!($a = getVal())), ale já přesto preferuji zápis bez závorek.

A do nového roku jedna příjemná zpráva nakonec. Už jsem před časem psal, že do trunku php se dostal patch, který umožní syntaxi dereferencování polí u volání funkce, která pole vrací.

$surname = $this->getNames()["John"];

Nově bude také operátor přístupu k vlastnostem a metodám objektu možné použít s operátorem new, např. dnes se musí psát

$tmp = new MyClass($param);
$foo = $tmp->bar;

nově bude možné

$foo = new MyClass($param)->bar;

viz. wiki - http://wiki.php.net/rfc/instance-method-call, přičemž obě tato syntaktická vylepšení lze dokonce kombinovat.


Komentáře (10)

  1. paranoiq - 29. 12. 2010 18:01

    ad implicitní return NULL - prázdné returny na konci některých funkcí z jednoho prostého důvodu nechávám - zjednodušuje to čtení kódu. na první pohled je jasné, že funkce nic vracet nemá. samozřejmě jen tam kde to má smysl

    ad

    if (!$a = $this->getCount())
    podobně obskurní a přesto funkční je i zápis
    if (!$a instanceof SomeClass)
    ale stejně neznám lepší možnost. uzávorkování to moc nezpřehlední

    ad new: jedno z pravidel v tom RFC je celkem děsivé:
    new $bar->y()->x should be read as (new ($bar->y)())->x
    
    doufejme, že nedojde k tomu, aby to bylo v téhle podobě implementováno. autorovi jsem poslal návrh aby určení jména třídy bylo ukončeno prvním výskytem ->

  2. koubel - 30. 12. 2010 00:53

    [1] - implicitní NULL - mě nepřijde dobré tam return NULL někdy psát a někdy ne právě z toho důvodu, že to čtenáře podvědomě nutí bádat nad tím proč tam někdy je return NULL a proč někdy ne.

    - if (!$a instanceof SomeClass) není s ničím v kolizi bych řekl, instanceof operátor má větší prioritu než ! takže je to stejné jako if (!($a instanceof SomeClass)), kdežto s if (!$a = func()) je to jinak - teoreticky mělo mít ! přednost před =, ale ve skutečnosti je to naopak, proto si speciálně tento případ vysloužil i explicitní poznámku v php dokumentaci. Proto je if (!$a = func()) hodně kontroverzní a v podstatě hack, asi bych opravdu raději psal if (!($a = func())), to už je naprosto korektní.

    - nemyslím, že Ti to Felipe a ostatní uznají, mě to chování popsané Felipem přijde korektní - je v souladu s tím, jak se new chová dnes, jméno třídy u operátoru new je ohraničeno () pokud je tedy tato dvojce znaků přítomna. Jinak jsem kecal, v trunku to zatím není.

  3. SendiMyrkr - 30. 12. 2010 04:58

    Mně se líbí například:

    // pokud je splneno prvni vykona se druhe za &&
    $indexes = $this->getIndexes() && $firstIndex = $indexes[0];
    // pokud je splneno prvni, druhe se nevykona
    $indexes = $this->getIndexes() || $firstIndex = null;
    
    Používam to hlavně namísto jednořádkových podmínek...

  4. Vojtěch Vondra - 30. 12. 2010 12:16

    Napadá mě jedno špatné použití

    if ($var) { }
    , a to při kontrole prvků pole, resp. pokud si člověk není jist, že index existuje. Patří to sice do mnohem větší kategorie chyb při vypnutém E_NOTICE, ze zkušeností je to ale z nich nejčastější.

  5. koubel - 30. 12. 2010 14:29

    [4] pokud chci v podmínkách pracovat s hodnotami v polích a nevím zda existují, pak samozřejmě

    žádné if ($arr[4]) {}
    ale asi nejlépe if (!empty($arr[4])) {}
    
    myslím, že empty, stejně jako isset by mělo fungovat i pro indexy polí. Kód rozhodně psát jako E_STRICT kompatibilní bez tolerance jakýchkoli notices.

  6. David Grudl - 30. 12. 2010 20:39

    Slabé typování může být velice zrádné, neboť i neprázdný řetězec se může vyhodnotit jako falsy. Porovnávací operátory doporučuji naopak psát vždy jako === nebo !== a pokud záměrně chci využít slabého typování a skutečně vím, proč to dělám, použiju == či != a doplním kód vysvětlujícím komentářem.

    ad 5: empty() je něco docela jiného, sem patří jednoznačně isset() nebo array_key_exists(), podle toho, jak pracujeme s hodnotou null.

  7. koubel - 31. 12. 2010 11:51

    [6] - věc názoru, já mám jinou zkušenost a slabé typování považuji za přednost, o tom je velká část článku. Že "0" odpovídá po přetypování na bool false patří dle mého právě k obrovským výhodám a využívám toho kde mohu. Z toho názoru plyne i to, co jsem odpověděl v [6]. Tvá zkušenost je asi jiná, přít se o tom nehodlám.

  8. paranoiq - 31. 12. 2010 12:36

    [2] Felipe mi odepsal, že to v podstatě není návrh, ale popis aktuálního chování (nejspíš v trunku, v poslední ostré to nefunguje). v budoucnu pravděpodobně bude možná jen varianta se závorkami

  9. Martin Calta - 9. 6. 2011 11:29

    Zápis if ($a = $this-getVar()) je pro mě nepřehledný. Když člověk hledá chybu zejména v cizím kódu, tak si nikdy není jistý, jestli to byl záměr nebo chyba. I NetBeans u takového zápisu upozorňují, že je to vo hubu a mají pravdu :-).

    Pokud jde o slabé typování, tak to samozřejmě využívám taky, ale všeho s mírou. Není možné to cpát všude, jinak vznikají zbytečné chyby. Např. zrovna teď jsem narazil na místo, kde jsi to nepoužil úplně správně :). Tady se podle mě bez === nedá obejít:

    class Identity implements IIdentity {
      ...
      private $permissions;
      ...
      public function getPermissions() {
        if ($this->permissions) return $this->permissions;
    
        $permissions = array();
        foreach (dibi::select(...) as $permission) {
          $permissions[] = $permission->permission_name;
        }
    
        return $permissions
          ? $this->permissions = $permissions
          : null;
      }
    }
    
    Ta podmínka na začátku getPermissions() má zajistit, aby se select nevolal při každém zavolání té metody, ale jen poprvé. Jenže co když uživatel žádná práva nemá? Dotaz nevrátí nic a v $this-permissions zůstane NULL. Takže se pak select bude stejně volat pokaždé. Proto jsem metodu upravil tak, že do $this-permissions při prvním zavolání nastavím array(). Podmínka if ($this-permissions) by ale nerozlišila, jestli je tam NULL nebo array(), takže tam prostě musím použít if ($this-permissions !== null).

    Kromě toho se mi moc nelíbí, když funkce vrací někdy pole a někdy NULL. Připadá mi lepší, když vrací pokaždé pole (i když je prázdné) a před jeho procházením pak není nutné pokaždé kontrolovat, jestli je to opravdu pole. Předělal jsem to tedy takto:
    class Identity implements IIdentity {
      ...
      private $permissions;
      ...
      public function getPermissions() {
        if ($this->permissions !== null) return $this->permissions;
    
        $this->permissions = array();
        foreach (dibi::select(...) as $permission) {
          $this->permissions[] = $permission->permission_name;
        }
    
        return $this->permissions;
      }
    }
    

  10. koubel - 22. 6. 2011 17:24

    [9] Martine,
    U

    if ($a = $this->getVar())
    v kontextu ostatního kódu bývá jasné o co jde a není moc důvodů bádat nad chybou. Takováto chyba se navíc projeví v testech většinou hned, s testy opravdu není co řešit. Je fakt, že je to subjektivní záležitost, je to hlavně jen o zvyku, v IDE se to upozorňování dá samozřejmě vypnout.

    Jinak s tím příkladem na === je to samozřejmě pravda, tady je to jednoznačné, byla to chyba. Rozhodně jsem tím nemyslel brát to jako dogma.

    S tím vracením prázdného pole moc nesouhlasím, protože se to špatně explicitně vyjadřuje v phpdoc, podle které IDE napovídá.
    @return array|null je jasné hned, kdežto v případě @return array není jasné, že fce může vrátit prázdné pole nebo ne. Vždy záleží na okolnostech, co je lepší, je to spíš drobnost.

Komentáře jsou uzavřeny.