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:
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:
Praktisch wären solche Klassen etwa für Typehints:
Ein etwas komplexeres Beispiel:
<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.)
Realisiert habe ich das derzeit über eine Klasse Generics_Factory, die sich als Autoloader registriert:
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:
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:
index.php mit Autoload-Funktion für Klassen in ./lib:
Templates liegen standardmäßig im Verzeichnis ./signatures. Zwei habe ich bereits oben vorgestellt, hier ein drittes:
./signatures/<X>_<Y>_Map.gphp:
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
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"); } } }
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());
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) {}
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) { } }
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));
Code:
[0] type: class parent: null /* Keine Elternklasse (<X:Foo>) angegeben */ name: X [1] type: const parent: null name: Set
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($part, 0, 1) === '<') {
$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($parts, 1);
$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;
}
}
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
./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