Gyors és “típusbiztos” kivételkezelés PHP-vel


Ebben a rövid tutorialban bemutatok egy módszert, amellyel anélkül dobálhatunk tetszőleges osztályú kivételeket, hogy azokat előre be kéne töltenünk (és leprogramoznunk).

A megfelelő hibakezelés régóta probléma volt a programozási nyelvekben, a C++ idejében (pontosabban már valamivel korábban, megfelelő forrás híányában most maradjunk a C++-nál, úgyis annak a szintaxisát örökölte a php) nyelvi szinten bevezettek erre egy mechanizmust, az ún. exception-öket (kivételek).

A legtöbb kivételeket támogató nyelvben a nyelv alap csomagjai többnyire rengeteg kivételt definiálnak, mivel a kivétel típusa (vagy osztálya, nyelvtől függően) további információval szolgál a hibáról – és ami lényegesebb, a vezérlés további sorsáról.

Egy Java példa:

1
2
3
4
5
6
7
8
9
10
11
try {
   double x = 1 / Integer.parseInt(args[1]);
} catch(ArrayIndexOutOfBoundsException exception) {
   /* Nem adták meg a parancssori paramétert */
} catch(NumberFormatException exception) {
   /* Nem számot adtak meg */
} catch(DivisionByZeroException exception) {
   /* Nullát adtak meg, azzal nem lehet osztani */
} catch(Exception exception) {
   /* Valami egyéb hiba */
}

A lényeg gondolom érthető. PHP-ben ez egy kicsit nehézkesebb lenne, mivel ahhoz, hogy ezt így használjuk 3 új osztályt (ArrayIndex.., NumberFormat… és DivisionBy…) kéne definiálnunk – csak azért, hogy kihasználhassuk a kivételkezelés előnyeit.

Szerencsére a PHP fejlesztői nem úgy kódolták le az értelmezőt, hogy ha nem létező osztályú Exception-t próbálunk elkapni, akkor hibát dobjon, így kicsit szabadabban játszhatunk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class cExceptionGenerator {
 
    private static $generatedInstances = array();
 
    public static function generate($className) {
        if(isset(self::$generatedInstances[strtolower($className)])) {
            return self::$generatedInstances[$className];
        }
        if(!class_exists($className, false)) {
            eval('class ' . $className . ' extends Exception { }');
            return self::$generatedInstances[strtolower($className)] = $className;
        } else {
            return 'Exception';
        }
    }    
 
    public static function generateAndThrow($className, $message, $code = null) {
        $class = self::generate($className);
        throw new $class($message, $code);
    }
}

Ez az osztály futás közben (az eval-al) hozza létre a kivétel osztályainkat, megfelelő kóddal csak akkor, amikor tényleg szükségünk van rá. Így például a fenti Java kód PHP-beli megfelelője:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
try {
  if(!isset($argv[1])) {
      $class = cExceptionGenerator::generate('ArrayIndexOutOfBoundsException');
      throw new $class('Nincs 1. elem');
  }
  if(is_numeric($argv[1]) == false) {
      $class = cExceptionGenerator::generate('NumberFormatException');
      throw new $class('Ervenytelen szamformatum');
  }
  if((int)$argv[1] == 0) {
      $class = cExceptionGenerator::generate('DivisionByZeroException');
      throw new $class('Ize...');
   }
   $x = (int)$argv[1] / 2;
} catch(ArrayIndexOutOfBoundsException exception) {
   /* Nem adták meg a parancssori paramétert */
} catch(NumberFormatException exception) {
   /* Nem számot adtak meg */
} catch(DivisionByZeroException exception) {
   /* Nullát adtak meg, azzal nem lehet osztani */
} catch(Exception exception) {
   /* Valami egyéb hiba */
}

Futás közben ez a megfelelő ágon fog tovább haladni, és csak azokat a kivételeket kellett előállítani, amelyeket valóban használunk is. A másik metódussal (generateAndThrow) rövidebb kódot is lehetett volna írni (pl.: isset($argv[1]) or cExceptionGenerator::generateAndThrow(…)) viszont ekkor a “dobás helye” (ami pontosabban a létrehozás sora) a cExceptionGenerator::generateAndThrow metódus törzsében lesz, így ha nem kapjuk el a, akkor a stack trace-t kell vizsgálnunk hogy megtudjuk, valójában honan jött a kivétel.

Megjegyzés: ugyanez a működési elv elérhető az autoload funkcionalitás kihasználásával, azonban ennek több hátránya is van:

  • A szerkesztők nem tudják feloldani a nevet, így nincs auto complete
  • Nehezebben olvasható lesz a kód, mivel nem létező osztályokra hivatkozunk benne
  • Más fejlesztő/hosszabb-rövidebb kihagyás után sokáig keresgélhetjük az adott kivételosztályokat, míg a kivétel-generáló osztályok esetében elsőre látható (vagy pár kattintás után megnézhető) hogy honnan jön az új osztály

Mindenesetre:
PHP 5.2 és korábbi változatokhoz

1
2
3
4
5
6
7
8
spl_autoload_register(create_function('$class', "return eval('class ' . \$class . ' extends Exception {}')"));
 
/* és egy ellenőrzés: */
try {
	throw new GeneratedException('Class generated on the fly');
} catch(GeneratedException $x) {
	echo $x;
}

Ugyanez PHP 5.3-al és lambda függvényekkel

1
2
3
spl_autoload_register(function($classname) {
	return eval('class ' . $classname . ' extends Exception {}');
});
Kapcsolódó bejegyzések:
  • A PHP-ben az objektumtulajdonságok beállítására van egy beépített lehetőség (__get() és __set() metódusok), azonban ezek nem típusbiztosak, így ajánlott saját ge …

  • Aki írt már JavaScript kódot tudja, hogy a nyelv rengeteg objektummal dolgozik, és minden objektumnak vannak böngésző-specifikus tulajdonságai, metódusai. Ilyen …

  • Azt tudni kell, hogy sok módon meg lehet írni egy programot. A mi feladatunk az, hogy kiválasszuk közülük a leghatékonyabbat. A mi kódolónk jelen esetben egy XOR …

  • Néha jól jöhet, ha tudjuk, hogy egy-egy függvényünket milyen környezetből (milyen függvényből, mely osztály mely metódusából, melyik fájlból stb.) hívták meg. Eb …

  • Ebben a tutorialban bemutatok egy BB kód értelmezőt, valamint annak a használatát. Remélem hasznos lesz számotokra. A kódot csak kimásolod innen egy fájlba és gy …

A cikket beküldte: BlackY ()

3 hozzászólás

  1. BlackY says:

    Az 5.3-as kódhoz azért még hozzátartozik, hogy névtér kezeléssel együtt egy kicsit bonyolódik a kód:

    if(!defined('NAMESPACE_SEPARATOR')) {
    	define('NAMESPACE_SEPARATOR', '');
    }
    spl_autoload_register(function($classname) {
    	list($namespace, $classname) = explode(NAMESPACE_SEPARATOR, $classname);
    	if(!$classname) {
    		$code = 'class ' . $namespace . ' extends Exception {}';
    	} else {
    		$code = 'namespace ' . $namespace .'; class ' . $classname . ' extends Exception {}';
    	}
    	return eval($code);
    });

    BlackY

  2. Semmu says:

    ez nagyon jó, köszi :)

  3. Ez nagyszerű! Köszi!

Szólj hozzá
a Gyors és “típusbiztos” kivételkezelés PHP-vel c. bejegyzéshez

- Engedélyezett HTML elemek: <a> <em> <strong> <ul> <ol> <li>
- Forráskód beküldéséhez tedd a kódot ezek közé: <pre lang="php" line="1">Kódrészlet helye itt</pre>