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 {}');
});

3 HOZZÁSZÓLÁS

  1. 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

HOZZÁSZÓLOK A CIKKHEZ

Kérjük, írja be véleményét!
írja be ide nevét