ugrás a tartalomhoz

PHP - kivételek szerializálása

inf · 2012. Jún. 21. (Cs), 06.08
Üdv.

Nem akartam ennek külön blog bejegyzést, mert annyira kis dolog, úgyhogy ide teszem be.

class Clean_Exception extends Exception {

    public function __sleep() {
        return array_keys(get_object_vars($this));
    }

}
Ha sima Exception-t akarunk szerializálni, vagy annak valami leszármazottját, akkor az egész stack trace-t is szerializálja a php az összes benne lévő változóval. Ez vicces dolgokat tud okozni, mondjuk amikor a munkamenet kezelő objektumok meg hasonlók is belekerülnek...

Egyébként mindez űrlap validáláshoz kell nekem. Jelenleg úgy oldom meg, hogy a validator-tól el lehet kérni egy kivétel példányt, ha hiba van, és utána eldobni, vagy menteni, attól függően, hogy mi a szitu... Felhasználóbarátabb űrlapoknál így vissza lehet küldeni az eredeti űrlaphoz munkamenet változókban a hiba okát. Azért ezt a módszert választottam, mert működik rá az automatikus kiegészítés, és lehet nézni a típusokat. Ha mondjuk típuskóddal, vagy bármi mással küldeném vissza a hiba okát, az sokkal nehezebben kezelhető lenne.
 
1

Miért kivétel, miért nem egy

tgr · 2012. Jún. 24. (V), 10.47
Miért kivétel, miért nem egy sima hibaosztály? A kivételkezelés lassú.
2

Hozok egy példakódot,

inf · 2012. Jún. 24. (V), 11.23
Hozok egy példakódot, kíváncsi vagyok hogyan változtatnál rajta:

class WebShop_User_Create extends WebShop_Controller_AbstractNotAuthorizedController {

    public function build() {
        parent::build();
        $this->inputReader->configureAll(array(
            'email' => array(
                'path' => array('field', 'email'),
                'transformer' => new Clean_View_Html_Transformation_SpecialCharacters_Entities(),
                'validator' => new Clean_Validator_Logical_And(array(
                    new Clean_Validator_String_MinLength(6),
                    new Clean_Validator_String_Email()
                ))
            ),
            'password' => array(
                'path' => array('field', 'password'),
                'transformer' => new Clean_View_Html_Transformation_SpecialCharacters_Entities(),
                'validator' => new Clean_Validator_String_MinLength(1),
            ),
            'password2' => array(
                'path' => array('field', 'password2'),
                'transformer' => new Clean_View_Html_Transformation_SpecialCharacters_Entities()
            )
        ));
    }

    public function execute() {
        try {
            $this->inputReader->touchAll(array('email', 'password', 'password2'));
            if ($this->inputReader->get('password') !== $this->inputReader->get('password2'))
                throw new Clean_Collection_Data_InvalidInputException(new Clean_Collection_Map_Map(array(
                            'password2' => new WebShop_User_Model_PasswordTypoException()
                        ))
                );

            $user = new WebShop_User_Entity();
            $user->setEmail($this->inputReader->get('email'));
            $user->setPassword($this->encrypter->transform($this->inputReader->get('password')));

            $model = new WebShop_User_Model_Create();
            $model->setUser($user);
            $this->exec($model);

            $this->exec(new WebShop_User_View_OrderRedirection());
        } catch (Clean_Collection_Data_InvalidInputException $e) {
            $this->invalidRequest($e);
        } catch (WebShop_User_Model_AlreadyExistException $e) {
            $this->invalidRequest($e);
        }
    }

    public function invalidRequest(Clean_Exception $e) {
        $this->session->put('referer', array(
            'exception' => $e,
            'rawMap' => new Clean_Collection_Map_Map($this->inputReader->getAllRaw(array('email', 'password', 'password2')))
        ));
        $this->exec(new WebShop_User_View_InvalidRegistrationRedirection());
    }

}
Ez egy action, a redirection átviszi egy másik, az űrlapot kirajzoló action-re a kérést (ugye http location header-rel), és azon az oldalon instanceof-al lehet ellenőrizni, hogy melyik kivételeket dobta az itteni kód, és kirajzolni az alapján a hibaüzenetet a megfelelő input mellé.

Na most nekem ebben a megoldásban két dolog tetszik. Az egyik, hogy a kivételt bármilyen mélységből eldobhatom, a másik meg, hogy az instanceof-ra működik az automatikus kiegészítés, tehát nem tudom elgépelni a kivétel nevét, úgy, mint mondjuk egy hiba típus nevét. Nem utolsó sorban nem kell azzal vesződnöm, hogy hibakódot vagy hiba típus nevet adjak. Nyilván ha ilyet csinálok, akkor azt már nem lehet refaktorálni normálisan, mert az string-ben lesz, tehát sokkal nehezebben módosítható.
3

Hozok egy példakódot,

tgr · 2012. Jún. 24. (V), 13.53
Hozok egy példakódot, kíváncsi vagyok hogyan változtatnál rajta:


Először is mindig saját magára submitolnám a formot, így hibás kitöltés esetén nem kell redirektelni. Ennek usability szempontból is van előnye, mert POST requestnek látja a browser, és blokkolja a navigációt/újratöltést (amivel elveszhetnek a már kitöltött mezők), meg sessionnel sem kell bohóckodni, hanem a form megjelenítésekor még megvannak a hibaellenőrzéskor generált adatok, csak ki kell írni őket a megfelelő mezők fölé.

Ez viszonylag könnyen automatizálható, így ha a standard "pirossal kiírjuk, hogy melyik mezőt kell javítani) megoldás jó, akkor egyáltalán nem kell kézzel error handlert írni, azt a form osztályod automatikusan le tudja kezelni; ráadásul újrafelhasználható lesz a kód olyan környezetben is, ahol a redirektnek nincs értelme (pl. egy SOAP API-ban). Ha maradsz a kivételdobásos megoldásnál (aminek a hátránya, hogy a rendszer alapos ismerete nélkül nehéz megmondani, hogy mikor hova kerül a vezérlés), akkor is el lehet hagyni a mindent magábafoglaló try blokkot, és a hibakezelő hívását intézheti a controller ősosztály.

De inkább valami olyasmit csinálnék, hogy a formot definiáló kód végén van egy if, ami ellenőrzi, hogy sikeres-e a validáció, és ha igen, akkor átirányít valahová, ha nem (vagy még nem is lett elposztolva a kód), akkor meg simán megjeleníti a formot (és a form automatikusan lekezeli a hibák megjelenítését, nem kell semmi dobni sehova). Ennek előnye, ha pl. többféle hiba is van (a form validálásból is jön hibád, meg valami kézi forrásból is), akkor könnyen össze tudod olvasztani őket, ha van a formnak valami hibát regisztráló metódusa, míg kivételnél ezt elég nehézkes lenne megtenni (-> több lépésben kapja csak meg a user a hibákat -> rossz usability).

Plusz (bár ennek már semmi köze a kivételekhez) nem szeretek a kontrollerben sokat gépelni, a modell mezőkhöz kötném a validátorokat, és lehetőséget adnék arra, hogy a form mezőket a modell mezőkből lehessen létrehozni, és ilyenkor automatikusan rájuk kerülnének a megfelelő validátorok (beleértve a duplán bekért mezők egyenlőségét).

Nyilván ha ilyet csinálok, akkor azt már nem lehet refaktorálni normálisan, mert az string-ben lesz, tehát sokkal nehezebben módosítható.


Simán lehetnek attól még a hibáid objektumok, hogy valami konténer objektumban tárolod őket, és nem egyesével dobálod. (instanceof-ozni ugyan ettől még nem fogsz tudni, de azt nem is nagyon lehet, ha egyszerre sok hibát akarsz kezelni.) De a legtöbb IDE amúgy is felajánlja refaktoráláskor az osztálynévvel egyező stringek módosítását. (Szükség is van rá, mert dokumentáció stb.)
4

Az első fele megfontolandó,

inf · 2012. Jún. 24. (V), 18.36
Az első fele megfontolandó, mármint hogy saját magának postoljon, és csak akkor irányítson át, ha sikeres volt a post. Próbáltam valahogy CRUD szerint tagolni, mármint ugye egy űrlap kirajzolása az gondolom read-nek számít, viszont az elküldése az update vagy create... Igazából ebbe még annyira nem folytam bele. Ha saját magának postol, az tényleg leegyszerűsíti a dolgot. Ami viszont egy kicsit zavar benne, hogy vissza gombnál, elévült lesz az oldal.

Plusz (bár ennek már semmi köze a kivételekhez) nem szeretek a kontrollerben sokat gépelni, a modell mezőkhöz kötném a validátorokat, és lehetőséget adnék arra, hogy a form mezőket a modell mezőkből lehessen létrehozni, és ilyenkor automatikusan rájuk kerülnének a megfelelő validátorok (beleértve a duplán bekért mezők egyenlőségét).

Na ezzel nem igazán értek egyet. Mármint én a model-ben is validálok osztályra és típusra, de előtte nyilván transzformálni kell az adatokat, mert string-ként kapom meg őket, nem integer, boolean, stb... formában, és akkor még a fájl feltöltésről nem is beszéltünk... Ez az inputReader-es rendszer elég jól bevált ilyen szempontból. Azt hiszem egy CMS-et is rá lehetne tákolni, idővel, mert körülbelül az a séma a weblapoknál, hogy az adatok 4 formában vannak jelen. Egy, ami bejön input-on, kettő, amit kirajzolsz, három, ami az adatbázisban van, és négy az objektum háló, amit a model használ. Az esetek 90+%-ában csak arról van szó, hogy ezeket az adatokat egyik formából a másikba alakítod, és közben validálsz, ha mondjuk input-ról van szó. A maradék meg a tényleges alkalmazás logika.
5

Ha a modellben nyilván van

tgr · 2012. Jún. 24. (V), 21.20
Ha a modellben nyilván van tartva a típus, akkor abból le tudod automatikusan generálni a megfelelő input transzformátort is. Sokkal olvashatóbb lesz a kód, ha annyi a form/validátor definíciód, hogy

$user = new WebShop_User_Entity();
$generator = new Entity_FormField_Generator($user);
$this->inputReader->configureAll(array(
    'email' => $generator->createField('email'),
    'password' => $generator->createField('password'), 
    'password2' => $generator->createFieldRepetition('password'),
));
és centralizálod vele a validálási kritériumok kezelését, ha változik pl. egy mező megengedett hossza, ami elég gyakori dolog, akkor elég a modellben átírni, nem kell az összes kontrollert végigtúrni, hogy hol szerepel az adott mező űrlapban.
6

és centralizálod vele a

inf · 2012. Jún. 25. (H), 03.44
és centralizálod vele a validálási kritériumok kezelését, ha változik pl. egy mező megengedett hossza, ami elég gyakori dolog, akkor elég a modellben átírni, nem kell az összes kontrollert végigtúrni, hogy hol szerepel az adott mező űrlapban.


Igen, ezt látom én is, csak még nem ennyire fejlett a keretrendszerem, hogy össze tudjam vonni egy DIC-ben ezeket az adatokat. Végső soron tényleg ez a cél.