Ankündigung

Einklappen
Keine Ankündigung bisher.

Generische Programmierung (Generics)/Parametrisierte Typen via Autoloading

Einklappen

Neue Werbung 2019

Einklappen
X
  • Filter
  • Zeit
  • Anzeigen
Alles löschen
neue Beiträge

  • Generische Programmierung (Generics)/Parametrisierte Typen via Autoloading

    Hi.

    Mir kam vorhin eine Idee, wie sich in PHP Ansätze für parametrisierte Typen/Generics über automatische Codeerzeugung via Autoloading umsetzen lassen. Diese Idee möchte ich hier vorstellen.

    (Siehe etwa auch: http://www.php.de/php-fortgeschritte...erics-php.html)

    Ein Beispiel-Template mit einem generischen Klassen-Platzhalter:

    Code:
    // Datei: <X>_Set.gphp
    class Gen_<X>_Set
    {
        protected $_data = array();
    
        public function add(<X> $obj)
        {
            $this->_data[] = $obj;
        }
    
        public function length()
        {
            return count($this->_data);
        }
    
        public function get($index)
        {
            $index = (int) $index;
    
            if (isset($this->_data[$index])) {
                return $this->_data[$index];
            } else {
                throw new IllegalArgumentException("Index does not exist");
            }
        }
    }
    Instanzen dieser Klassen verwalten jeweils eine Liste von Objekten vom Typ <X>. (Der Code ist hier zur Verdeutlichung einfach gehalten. Normalerweise sollte ArrayAccess implementiert werden usw.) Beispielsweise:

    Code:
    $set = new Gen_Foo_Set(); // Set von Foo-Instanzen
    $set->add(new Foo('bar'));
    $set->add(new Foo('baz'));
    
    // Das hier wirft einen Fehler
    $set->add(new Bar());
    
    // Das hier geht
    $set2 = new Gen_Bar_Set(); // Gen_* ist ein willkürliches Präfix für parametrisierte Typen, um 
                               // nicht fremden Namespaces (MyLibrary_Set) in die Quere zu kommen
    $set2->add(new Bar());
    Praktisch wären solche Klassen etwa für Typehints:

    Code:
    // In $users kann alles stehen
    function doSomethingWithUsers(array $users) {}
    
    // vs.
    
    // In $users können nur Instanzen von User stehen
    function doSomethingElseWithUsers(Gen_User_Set $users) {}
    Ein etwas komplexeres Beispiel:

    Code:
    // Datei: <X:Bar>_abc_<Y:Foo>_abc_<Z>.gphp
    class Gen_<X:Bar>_abc_<Y:Foo>_abc_<Z>
    {
        public function __construct(<Z> $z, <Y> $y)
        {
    
        }
    }
    <X:Bar> bedeutet, der Typ, der für <X> angegeben wird, muss von der Klasse "Bar" abstammen. Analog muss <Y> von "Foo" abstammen. (Ich bin mir nicht sicher, ob dort bereits Vererbung über mehrere Generationen unterstützt wird. Das hängt davon ab, ob die entsprechende PHP-Standardfunktion das tut oder nicht.)

    Code:
    // Das klappt
    $o = new Gen_BarChild_abc_FooChild_abc_Foo($foo, $fooChild);
    
    // Das nicht
    $o = new Gen_FooChild_abc_BarChild... usw.
    * * *

    Realisiert habe ich das derzeit über eine Klasse Generics_Factory, die sich als Autoloader registriert:

    Code:
    $gf = new Generics_Factory();
    $gf->registerAsAutoloader();
    $o = new Gen_BarChild_abc_FooChild_abc_Foo(new Foo(1), new FooChild(2));
    Bei Instanzierung von Generics_Factory werden die Dateinamen der vorhandenen Template-Dateien (*.gphp) eingelesen, in ihre Bestandteile zerlegt und gespeichert. Aus Gen_<X>_Set würde etwa:

    Code:
    [0] type: class
        parent: null        /* Keine Elternklasse (<X:Foo>) angegeben */
        name: X
    [1] type: const
        parent: null
        name: Set
    Beim Versuch, nun etwa eine Klasse Gen_Foo_Set zu instanzieren, wird der Klassenname stückweise mit allen verfügbaren Signaturen verglichen. Anteile vom Typ "class" werden auf das Vorhandensein der Klasse bzw. auch der Elternklasse geprüft (bei Bedarf werden diese automatisch geladen), konstante Bestandteile ("Set") werden per String-Vergleich auf Übereinstimmung geprüft.

    Wird nur eine mögliche Signatur gefunden, die auf den übergebenen Klassennamen zutrifft, wird die zugehörige Template-Datei eingelesen, die Platzhalter (<X>) werden durch die entsprechenden Klassennamen (Foo) ersetzt und der Code wird evaluiert (eval()-Funktion).

    Alternativ kann der erzeugte Code zurückgegeben werden, um etwa in normale PHP-Klassendateien geschrieben zu werden.

    * * *

    ./lib/Generics/Factory.php:

    PHP-Code:
    <?php

    class Generics_Factory
    {
        protected 
    $_signatures         = array();

        protected 
    $_pathToSignatures   '';

        protected 
    $_genericClassPrefix 'Gen';

        public function 
    __construct($pathToSignatures './signatures')
        {
            
    $this->_pathToSignatures $pathToSignatures;
            
    $this->_loadSignatures();
        }

        public function 
    registerAsAutoloader()
        {
            
    spl_autoload_register(array($this'_load'));
        }

        
    /**
         *
         * @todo Does not support underscores in parent class identifiers
         * (e. g. <X:Foo_Bar>)
         */
        
    protected function _loadSignatures()
        {
            foreach (
    glob($this->_pathToSignatures '/*.gphp') as $filename) {
                
    $signature = array();
                
    $tmp basename($filename'.gphp');
                
    $parts explode('_'$tmp);

                foreach (
    $parts as $part) {
                    if (
    substr($part01) === '<') {
                        
    $part trim($part'<>');
                        if (
    strpos($part':') !== false) {
                            
    $tmp explode(':'$part);
                            
    $signature[] = array('type'   => 'class',
                                                 
    'name'   => $tmp[0],
                                                 
    'parent' => $tmp[1]);
                        } else {
                            
    $signature[] = array('type'   => 'class',
                                                 
    'name'   => $part,
                                                 
    'parent' => null);
                        }
                    } else {
                            
    $signature[] = array('type'   => 'const',
                                                 
    'name'   => $part,
                                                 
    'parent' => null);
                    }
                }

                
    $this->_signatures[] = array('filename' => $filename,
                                             
    'content'  => $signature);
            }
        }

        protected function 
    _load($class)
        {
            
    $a $this->_findCorrectSignature($class);

            if (
    is_null($a)) {
                return;
            } else {
                list(
    $signature$parts) = $a;
            }

            
    $code $this->_createGenericClass($signature$parts);
            eval(
    $code);
        }

        protected function 
    _findCorrectSignature($class)
        {
            
    $parts explode('_'$class);

            if (
    $parts[0] !== $this->_genericClassPrefix) {
                
    // $class is not a generic class
                
    return null;
            }
            
    $parts array_slice($parts1);

            
    $possibleMatches = array();

            foreach (
    $this->_signatures as $signature) {
                if (
    count($signature['content']) === count($parts)) {
                    
    $possibleMatches[] = $signature;
                }
            }

            for (
    $i 0$i count($parts); $i++) {
                
    $newPossibleMatches = array();
                foreach (
    $possibleMatches as $match) {
                    switch (
    $match['content'][$i]['type']) {
                        case 
    'class':
                            if (
    is_null($match['content'][$i]['parent'])) {
                                if (
    class_exists($parts[$i], true)) {
                                    
    $newPossibleMatches[] = $match;
                                }
                            } else {
                                if (
    class_exists($parts[$i], true)
                                    && 
    class_exists($match['content'][$i]['parent'], true)
                                    && 
    strtolower(get_parent_class($parts[$i])) === strtolower($match['content'][$i]['parent'])
                                ) {
                                    
    $newPossibleMatches[] = $match;
                                }
                            }
                            break;
                        case 
    'const':
                            if (
    $parts[$i] === $match['content'][$i]['name']) {
                                
    $newPossibleMatches[] = $match;
                            }
                            break;
                        default:
                            die(
    'Something went horribly wrong.');
                            break;
                    }
                }
                
    $possibleMatches $newPossibleMatches;
            }

            if (
    count($possibleMatches) === 0) {
                throw new 
    Exception('No generic signature found for class "'
                        
    $class '"');
            } else if (
    count($possibleMatches) > 1) {
                throw new 
    Exception('Ambigious generic signatures found for class "'
                        
    $class '"');
            }

            return array(
    $possibleMatches[0], $parts);
        }

        protected function 
    _createGenericClass($signature$parts)
        {
            
    $code file_get_contents($signature['filename']);

            for (
    $i 0$i count($signature['content']); $i++) {
                
    $c $signature['content'][$i];

                if (
    $c['type'] === 'class') {
                    if (
    is_null($c['parent'])) {
                        
    $code str_replace('<' $c['name'] . '>'$parts[$i], $code);
                    } else {
                        
    $code str_replace('<' $c['name'] . '>'$parts[$i], $code);
                        
    $code str_replace('<' $c['name'] . ':' $c['parent'] . '>',
                                            
    $parts[$i], $code);
                    }
                }
            }

            return 
    $code;
        }

        public function 
    createCodeForClass($class)
        {
            list(
    $signature$parts) = $this->_findCorrectSignature($class);
            
    $code $this->_createGenericClass($signature$parts);
            return 
    $code;
        }
    }
    index.php mit Autoload-Funktion für Klassen in ./lib:

    PHP-Code:
    <?php

    spl_autoload_register
    (function ($class) {
        
    $file './lib/' str_replace('_''/'$class) . '.php';
        if (
    file_exists($file)) {
            require_once 
    $file;
        }
    });

    $gf = new Generics_Factory();

    $o = new Gen_Foo_Set();
    // o. ä.

    echo $gf->createCodeForClass('Gen_Foo_Set'); // Debugging/Caching
    Templates liegen standardmäßig im Verzeichnis ./signatures. Zwei habe ich bereits oben vorgestellt, hier ein drittes:

    ./signatures/<X>_<Y>_Map.gphp:

    Code:
    class Gen_<X>_<Y>_Map
    {
        protected $_data = array();
    
        public function add(<X> $key, <Y> $value)
        {
            $this->_data[] = array('key'   => $key,
                                   'value' => $value);
        }
    
        public function length()
        {
            return count($this->_data);
        }
    
        public function getValue(<X> $key)
        {
            foreach ($this->_data as $d) {
                if ($d['key'] === $key) {
                    return $d['value'];
                }
            }
    
            return null;
        }
    }
    * * *

    Der Code hat "Proof of Concept"-Qualität. Die Syntax der Signaturen macht es aktuell schwierig, die Template-Dateien mit einem PHP-Editor zu bearbeiten, da sie kein gültiger PHP-Code sind. Darüber habe ich noch nicht wirklich nachgedacht.

    Was haltet ihr von der Sache? Ist der Ansatz okay oder zu sehr Meta-Programmierung? (Und wo sind die zweifelsohne existierenden 100 Leute, die schon vor 10 Jahren auf dieselbe Idee gekommen sind? )

    Ich denke, einige Set- und Map-Strukturen werde ich auf diese Art testweise implementieren und nutzen. Derzeit erstelle ich immer mal wieder Mock-Objekte für typisierte Sets, die von irgendeiner abstrakten Elternklasse erben. (Das dürften viele kennen.) Die sind mit diesem Ansatz im Grunde überflüssig.

    Also, bitte schießen Sie los.

    Gruß Marc
Lädt...
X