"Objektumorientált" Drupal - A reflection osztályok haszna

Nem rég el kellett kezdenem drupalban fejleszteni. Számomra nagyon új és furcsa volt, hogy gyakorlatilag sehol egy igazi osztály, vagy az objektumorientáltság jele. Ez annyiban érdekes, hogy számomra egy jól strukturált OOP-re épülő forráskód sokkal érthetőbb, mint függvények sokasága, ahol ráadásul minden függvény ott lebeg a globális térben. Az is, ami kizárólag a modulnak kell. Ugyanez a változókkal. Na most kezdő drupalosként, hogy otthonosabban érezzem magam a forráskódban, megpróbáltam kierőszakolni az osztályok használatát úgy, hogy közben kigenerálom a függvényeket is, de csak azokat, amiket a drupalnak meg kell tudni hívni automatikusan. Hangsúlyozom, hogy a következő megoldást az ötlet és a megvalósítás miatt teszem közzé. Nem ajánlásként.

Megoldás

Modul írása általában

Drupalban, ha egy modult írunk, akkor lesz minimum egy modulneve.module nevű fájlunk. Ebben pedig olyan függvények, amik mind a modulneve_ prefix-szel kezdődnek. Azok a függvények pedig amik „privátok”, kapnak még egy aláhúzás jelet: _modulneve_. Ez után a prefix után pedig elég sok minden állhat. A rendszer pedig a bekapcsolt modulok függvényeit különböző helyeken meghívhatja. A függvények elnevezése persze kötött. Csak a prefix változik.

Ötlet

Az ötlet az volt, hogy valamilyen módon megkönnyítem a dolgom és osztályokat használok. Persze mivel példányra nincs szükség, csak statikusat. De már valami pluszt mégis kapok. Majd pedig megírom a szükséges függvényeket, és bennük egyetlen sor kódot írok. A hozzá tartozó metódus meghívását:

function modulneve_init()
{
     Modulneve::init();
}

Persze, ha vannak paraméterek, akkor azokat is ugyanúgy átadom. De akkor már jött a következő ötlet. Ezek a függvények így elég szépen automatizálhatók, hiszen ránézésre alig van különbség. Tehát rögtön megoldhatom, hogy már a függvényeket is az osztály automatikusan legenerálja.

A probléma

Egy részt valahogy mégsem árt jelölni, hogy ez az osztály egy modul osztálya. A modulnév elvileg lehet bármi. Akár php-ban foglalt szó is.

Illetve az szép, hogy emberi szemmel nagyon jól kivehető az elnevezések és paraméterezések hasonlósága, de a PHP-nak is meg kell tudni mondani, hogy hogyan adja át a paramétereket és egyáltalán a függvény paraméterlistáját hogyan építse fel. Referencia szerint kell-e átadni, elhagyható-e, mi az alapértelmezett értéke, stb. Ezt pedig neki automatikusan fel kell ismerni az osztályból, különben az egésznek semmi értelme.

Használható eszközök

Az előző cikkemben csak megemlítettem a Reflection osztályokat, de példát nem nagyon írtam. Na most ennél a feladatnál pont nagyon jó, hogy vannak.

A függvények generálásához pedig kell az eval is mindenképp.

A call_user_func_array függvény is kell, ami meghívja a generált függvényből a megfelelő metódust a paramétereivel.

És persze a jó öreg func_get_args(), hogy megtudjuk, milyen átadott argumentumai vannak a függvénynek.

Talán még a var_export() függvényt megemlítem, mert itt is jó szolgálatot tesz majd. A metódusok paramétereit le lehet kérdezni, de arról gondoskodni kell, hogy a függvényekben is megtartsák a típusukat. Lehetne ellenőrizni, hogy a paraméter default értéke string-e, vagy sem. És akkor idézőjelbe kell tenni. De ezzel egyszerűbb.

Megvalósítás

Először is úgy döntöttem, a modul osztályok neve a DrupalModules_Modulneve rendszert követi. Meg kell továbbá oldani azt, hogy csak a publikus metódusait generáljuk ki függvényalakba is. Ennek lekérdezését a

$ref = new ReflectionClass($class);
$methods = $ref->getMethods(ReflectionMethod::IS_PUBLIC);

megoldással el is érem. Kell viszont a függvény argumentumainak listája. Ráadásul olyan formában, ahogy azt eredetileg definiáltam. Ez egy összetettebb feladat, mert előbb le kell kérdezni a metódus paramétereit. Majd a paraméterek tulajdonságait. Majd ahhoz string formában legenerálni a paraméterlistát. Ezért ezt külön metódusba tettem.

A megoldás lelke: A ReflectionParameter osztály isPassedByReference metódusa megadja, hogy a változót referenciaként kell-e átadni. Az isDefaultValueAvailable és getDefaultValue metódusok pedig az alapértelmezett értékek lekérésben segítenek.

Ezzel így már két olyan metódusom lett, amit nem kellene függvényként legenerálni, de ebből az egyik mindenképpen publikus kell legyen. Amelyik elindítja az egész játékot. Ezért kitettem egy ősosztályba. Amit a modul osztálya örököl. Így a saját metódusainak és az átadott modulnévhez tartozó osztályok különbségét veheti. És csak azokat generálja le a publikusak közül. A megoldás tehát a következő:

class DrupalModules
{
       
        public static function generateFunctions($module)
        {
                $dm_methods = get_class_methods(__CLASS__);
                $class = 'DrupalModules_'.$module;
                $ref = new ReflectionClass($class);
                foreach ($ref->getMethods(ReflectionMethod::IS_PUBLIC) as $method)
                {
                        if (in_array($method->name, $dm_methods)) continue;
                        $argumentList = self::getMethodArgumentList($class, $method->name);
                        $callback = array($class, $method->name);
                        $eval = 'function '.$module.'_'.$method->name.'('.$argumentList.') {
                                $args = func_get_args();
                                call_user_func_array('
.var_export($callback,true).', $args);
                        }'
;
                        eval($eval);
                }
        }
       
        public static function getMethodArgumentList($class, $method)
        {
                $methodref = new ReflectionMethod($class, $method);
                $arguments = $methodref->getParameters();

                $list = array();
               
                foreach ($arguments as $argument)
                {
                        $argref = new ReflectionParameter(array($class, $method), $argument->name);
                        $argdef = '';
                        if ($argref->isPassedByReference()) {
                                $argdef .= '&';
                        }
                       
                        $argdef .= '$'.$argref->getName();
                       
                        if ($argref->isDefaultValueAvailable()) {
                                $defv = $argref->getDefaultValue();
                                $argdef .= ' = '.  var_export($defv, true);
                        }
                       
                        $list[] = $argdef;
                }              
                return implode(', ',$list);
        }
}

class DrupalModules_Modulneve extends DrupalModules
{
         public static  function init() {
              //modul inicializálás
         }

         public static function menu() {
               //modulhoz tartozó menük
         }

         //stb....
}

DrupalModules::generateFunctions('modulneve');

Értékelés

Elég sok Reflection példány jön létre, amik persze nem sokáig élnek, de sok metódusnál azért számíthat. Viszont kényelmes és szerintem jól szemlélteti a Reflection osztályok értelmét.

Osztályokkal kódolhatunk, de ettől még nem lesz objektumorientált a program. Ezért is tettem a címben idézőjelbe a kifejezést. A függvények továbbra is létezni fognak, de pluszban még a metódusokat is meg kell hívni, majd pedig eval-lal dinamikusan generálni őket.

Valahol olvastam egy olyan megjegyzést, hogy: „eval is evil”. Azaz az eval gonosz. Lehetőség szerint kerülni is kell a használatát, de ha kell, akkor kell.

Összességében tehát főleg az ötlet tetszik. És arra jó volt, hogy a kezdeti nehézségeken annyit könnyítsek, hogy barátságosabb forráskódom lesz, amiben otthon érzem magam. De ha az ember megérti és megszokja a drupal megoldásait, akkor talán jobb azt betartani. Ahol szükséges persze, ott lehet még belecsempészni egy kis OOP-t. De az automatikus generálgatás megspórolható.

Kategóriák: 
Megosztás/Mentés