Inicializace proměnných

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

Ve svých skriptech a funkcích zásadně inicializuji proměnné a nespoléhám se na to, že při prvním použití dostanou implicitní hodnotu (prázdný řetězec, nula, false, prázdné pole, apod.). Jednak to je účinná obrana proti podstrčení proměnné zvenku a jednak to zabraňuje těžko odhalitelným chybám, kdy je více souborů, které používají stejné proměnné, vloženo různě do sebe.

<?php
// špatný kód - kdokoliv může proměnnou $logged podstrčit zvenku nebo mohla být použita v jiném skriptu
if ($_POST["user"] == "root") {
    $logged = true;
}

// správný kód
$logged = false;
if ($_POST["user"] == "root") {
    $logged = true;
}

// úsporný kód
$logged = ($_POST["user"] == "root");
?>

Stejně důležité je i správně inicializovat pole, jak názorně ukazuje colder. Smutné je, že na nesprávně inicializované pole nás neupozorní ani E_NOTICE.

I přesto, že proměnné vždy inicializuji, tak nemám rád zobrazování chyb typu E_NOTICE. Důvod je ten, že při zapnutí těchto chyb nestačí inicializovat proměnnou typu pole, ale musí být inicializovány i jednotlivé prvky tohoto pole. Nejlépe to bude vidět na příkladu – dejme tomu, že chceme spočítat cenu výrobku v jednotlivých skupinách.

<?php
$vyrobky = array(
    1 => array("skupina" => 1, "cena" => 200),
    2 => array("skupina" => 2, "cena" => 300),
    3 => array("skupina" => 1, "cena" => 100),
); // pole s výrobky, kde klíčem je ID výrobku a hodnotou pole obsahující skupinu, do které výrobek spadá, a jeho cenu
$ceny_skupiny = array(); // výsledné pole, kde klíčem bude ID skupiny a hodnotou výsledná cena

// kód bez E_NOTICE
foreach ($vyrobky as $val) {
    $ceny_skupiny[$val["skupina"]] += $val["cena"];
}

// kód s E_NOTICE
foreach ($vyrobky as $val) {
    if (!isset($ceny_skupiny[$val["skupina"]])) {
        $ceny_skupiny[$val["skupina"]] = 0;
    }
    $ceny_skupiny[$val["skupina"]] += $val["cena"];
}
?>

Část if (!isset) kód pouze znepřehledňuje, vnáší do něj balast a kódu jako takovému ničím nepřispěje (na rozdíl od obecné inicializace proměnných).

Kód je nepřehlednější i v případě vypisování hodnot, u kterých se nedá spolehnout na to, že budou nastaveny:

<?php
// bez E_NOTICE
echo '<input name="search" value="' . htmlspecialchars($_GET["search"]) . '" />';

// s E_NOTICE
echo '<input name="search" value="' . (isset($_GET["search"]) ? htmlspecialchars($_GET["search"]) : "") . '" />';
?>

A teď si ještě představte, jak by kód vypadal bez použití ternárního operátoru, který se někomu špatně čte.

Viz také Hledání neinicializovaných proměnných.

Přijďte si o tomto tématu popovídat na školení Bezpečnost PHP aplikací.

Jakub Vrána, Dobře míněné rady, 25.2.2005, diskuse: 22 (nové: 0)

Diskuse

ikona llook:

To já zase na E_NOTICE nedám dopustit. Na položky, které "možná jsou" mám funkci:

<?php
function &item(&$array, $index, $default) {
    return (isset($array[$index])
        ? $array[$index]
        : $default
   
);
}
?>
Také řeší to, že jako implicitní hodnotu nechci vždy null.

ikona dgx:

Nemáš pocit, že ten článek trošku popírá sám sebe? Tedy v prvním odstavi jsi "pro" inicializaci, a hned v druhém týkajícím se polí ji popíráš.

Nespoléhej se na to, že Ti nikdo nemůže podstrčit  $ceny_skupiny[3] = 10;

ikona Jakub Vrána OpenID:

Je to tam myslím popsané jasně. Inicializace polí je samozřejmě správná věc stejně jako u jakýchkoliv jiných proměnných, ale inicializace jeho jednotlivých prvků je podle mě samoúčelná. Je to koneckonců i v příkladě:
<?php
$ceny_skupiny
= array();
?>
Díky tomu mám jistotu, že mi nikdo $ceny_skupiny[3] podstrčit nemůže.

bm:

A co operátor na potlačení chybových hlášení?
<?php
echo '<input name="search" value="' . htmlspecialchars(stripslashes(@$_GET["search"])) . '" />';
?>

Stefan:

Docela by me zajimalo, jak se daji podstrcit hodnoty vnitrnich promennych, tedy ne poli $_GET, $_POST apod.

ikona Jakub Vrána OpenID:

V zásadě dvěma způsoby: Při zapnuté direktivě register_globals zcela zvenku, za všech okolností z vložených skriptů (tady spíše nedopatřením než cíleným útokem):

<?php
include "connect.inc.php";
if (
$_POST["logged"] == "root") {
    $auth = true;
}
?>
connect.inc.php:
<?php
$auth
= "user";
$pass = "pass";
mysql_connect("localhost", $auth, $pass);
?>
Problém to může být hlavně u externích knihoven, které po sobě nechají nastavené nějaké pomocné proměnné.

Michal:

Zaujimave...

ikona Mackiee:

Zdravim,
snazim se jeden web odprostit od varovani E_NOTICE, avsak asi spravne nechapu, jak inicializovat pole. Mam napr. tento kod: (zjednodusenne)
<?php
// Moznosti stylu v config souboru
$STYLY = array("classic" => "Klasický styl",
               "mobil" => "Mobil / pda",
               "print" => "Pro tisk");

// Pole zakladnich nastaveni:
$_GET_DEFAULT = array( "styl" => "classic");

// z kontroly poslanych promenych:
$styl_nastaven = "ne";
     foreach($STYLY as $index => $hodnota){

        // jestlize je poslano get, nastavime cookie:
        if($_GET['styl'] == $index){

          setcookie("styl", $_GET['styl']);
          $styl_nastaven = "ano";

        }
    }
        // jestlize je poslano cookie, nastavime cookie:
        if(($styl_nastaven != "ano")){

          foreach($STYLY as $index => $hodnota){
            if($_COOKIE['styl'] == $index){
              $_GET['styl'] = $index;
              setcookie("styl", $_GET['styl']);
              $styl_nastaven = "ano";
            }

          }

        }

      // jinak default styl:
      if($styl_nastaven != "ano"){
        $_GET['styl'] = $_GET_DEFAULT['styl'];
        setcookie("styl", $_GET_DEFAULT['styl']);
      }
?>
Stale vsak dostavam varovani e_notice. Jedna o neinicializovane prvky pole? Nebo jen stale nechapu jak je inicializovat?

ikona Jakub Vrána OpenID:

Možná by stačilo si prohlédnout chybovou hlášku a zjistit, na kterém řádku k chybě dochází. Konkrétně to bude porovnávání $_GET['styl'] a $_COOKIE['styl']. To je potřeba nahradit něčím takovým: <?php (isset($_GET["styl"]) && $_GET['styl'] == $index) ?> nebo ještě lépe testem isset() obalit celý foreach cyklus.

ikona Mackiee:

Bravo! Obdivuji Tvou rychlost :)

Samozrejme ze to bylo hned na zacatku:
<?php
       
// jestlize je poslano get, nastavime cookie:
        if($_GET['styl'] == $index){
        }
        // spravne tedy melo byt:
        if(isset($_GET['styl']) && ($_GET['styl'] == $index)){
        //dalsi kod...
        }
?>

Dekuji, jsem zase o kousicek chytrejsi. Uz se Tesim, az si nekdy prijdu popovidat na nejake z Tvych jiste kvalitnich skoleni ;)

P.S. Nebylo by spatne, dat uzivateli pred odeslanim komentare nahled, jiste bych po sobe par veci jeste poopravil :)

Carlos:

Zdravím, zkouším a jsem z toho pořád nerozumný.

Původně jsem měl předání postu do proměné takto
<?php
$prijmeni
= uprav_vstup($_POST["prijmeni"];
?>

<?php
//obecne upraveni vstupu pro cp1250 - snad dobře ???
function uprav_vstup($hodnota)
{
    $hodnota = trim($hodnota);
    $hodnota = str_replace("\"","",$hodnota);
    $hodnota = str_replace("\'","",$hodnota);
    return($hodnota);
}
?>
Hlásí pochopitelně Notice: Undefined index: prijmeni

Snažil jsem se udělat podle rady, ale neuspěšně.
$prijmeni = false;
if (uprav_vstup($_POST["prijmeni"])) {
   $prijmeni = true;
}

Kde dělám chybu prosím? Děkuji

Michal:

Ne promin zapoměl jsem na tu fci, takto tedy:

<?php
 
function uprav_vstup($hodnota) {
    $hodnota = trim($hodnota);
    $hodnota = str_replace("\"","",$hodnota);
    $hodnota = str_replace("\'","",$hodnota);
    return($hodnota);
  }
  $prijmeni = isset($_POST["prijmeni"])?uprav_vstup($_POST["prijmeni"]):"";
?>

ikona v6ak:

Problém je jinde, chce to použít http://cz2.php.net/manual/en/function.array-key-exists.php .

Michal:

Já myslím že problém jinde není. Tak jako tak musí být proměnná $prijmeni definovaná takže když test neprojde přes isset či array_key_exists musíš stejně řešit dál.

ikona v6ak:

Doháje, nějak jsem si nevšiml isset. Ale isset nebo array_key_exists by to řešit mělo.

Já s tímto kódem problém nemám.

Michal:

Obě varianty jsou řešením, $_POST je také pole, takže proč nepoužít i array_key_exists, ale co ti přijde jednodušší:
<?php
  $prijmeni
= isset($_POST["prijmeni"])?uprav_vstup($_POST["prijmeni"]):"";
  //nebo
  $prijmeni = array_key_exists("prijmeni", $_POST)?uprav_vstup($_POST["prijmeni"]):"";
?>
Pro univerzálnost bych to ještě schoval do funkce, pokud se testuje více proměnných, příp. pole. 

Hever:

Dlouho jsem s tímto problémem bojoval, nesouhlasím s iniciováním prvků pole, proto jsem E_NOTICE nepoužíval.

Nejčastěji mě zajímá jestli ten který prvek pole obsahuje danou hodnotu a jen kvůli pravidlům E_NOTICE zapisovat např.

<?php
if(isset($_GET['seradit']) and $_GET['seradit']!='')
// místo
if($_GET['seradit'])
?>

mi přišlo samoúčelné a ničící přehlednost kódu.

Ale zároveň mě časem začaly zajímat ostatní E_NOTICE chyby, tak jsem to nakonec vyřešil ve metodě, která chyby odchytává a zpracovává (k odchytávání chyb využívám Nette Debug)

<?php
public static function _errorHandler($severity, $message, $file, $line, $context)
    {
    if(strpos($message,'Undefined index') === 0) return ;
    if(strpos($message,'Undefined variable: return') === 0) return ;
    ...
  }
?>

E_NOTICE si tedy zaznamenávám, ale chyby spojené s neiniciováním prvků pole zahazuji.

Stejně tak zahazuji chyby s neiciovanou proměnou $return, kterou používám v 80% metod - podstrčení hodnoty je nemožné a i kdyby se tak snad stalo, neohrozilo by to bezpečnost. Do $return vždy skládá výstupní string.

Michal Prynych:

Já celkem úspěšně používám konstrukci empty(), která řeší jak inicializovanost, tak i prázdný řetězec, nulu a false

<?php
if(!empty($_GET['seradit']))
?>

Hever:

Dal jsem příklad s prázdnou hodnotou, ale jindy chci porovnávat s nějakou danou hodnotou.
<?php
if($_GET['seradit']=='cena')
?>

Nebo vypsat prvek v poli a když neexistuje, tak nevypisovat nic.

<?php
echo isset($pole['prvni']['text']) ? $pole['prvni']['text'] : ''); //php 5.2 korektní
//versus
echo $pole['prvni']['text'];
?>

Michal Prynych:

Pak to samozřejmě použít nelze, spíš jsem chtěl říct, že je to šikovná konstrukce pro případy testování na true/false včetně kontroly na inicializovanost.

ikona Jakub Vrána OpenID:

U polí mě chování E_NOTICE také rozčiluje. Běžné proměnné (včetně $return) vždy inicializuji, takže tam mi to nevadí.

Tvé řešení je zajímavý nápad, ale dej pozor, abys to neschytal stejně jako já:

„Spoléhání se na text chyby je neuvěřitelný nesmysl“
http://php.vrana.cz/nacteni-neexistujiciho-souboru.php#d-10951

„řetězec je naprostý nesmysl a do aplikace bych si to rozhodně nezatahoval“
http://php.vrana.cz/nacteni-neexistujiciho-souboru.php#d-10977

„žádné parsování chybové zprávy, pokud není 100%ní záruka, že bse nezmění“
http://php.vrana.cz/nacteni-neexistujiciho-souboru.php#d-10988

Hever:

Výborně, PHP7 řeší problém s Undefined index pomocí Null Coalesce Operator
https://wiki.php.net/rfc/isset_ternary

Pár let jsem přečkal s "textem chybové zprávy", která se zatím nijak neměnila :) a definitivní řešení za chvíli vyjde v nové verzi.

Diskuse je zrušena z důvodu spamu.

avatar © 2005-2024 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.