Wie füge ich einem PHP-Objekt im laufenden Betrieb eine neue Methode hinzu?

98

Wie füge ich einem Objekt "on the fly" eine neue Methode hinzu?

$me= new stdClass;
$me->doSomething=function ()
 {
    echo 'I\'ve done something';
 };
$me->doSomething();

//Fatal error: Call to undefined method stdClass::doSomething()
MadBad
quelle
Ist dies speziell ohne Verwendung einer Klasse?
Thomas-Peter
1
Sie könnten __call verwenden. Verwenden Sie jedoch bitte nicht __call. Das dynamische Ändern des Verhaltens eines Objekts ist eine sehr einfache Möglichkeit, Ihren Code unlesbar und nicht wartbar zu machen.
GordonM
Sie können dieses Tool verwenden: packagist.org/packages/drupol/dynamicobjects
Pol Dellaiera

Antworten:

93

Sie können __calldies nutzen:

class Foo
{
    public function __call($method, $args)
    {
        if (isset($this->$method)) {
            $func = $this->$method;
            return call_user_func_array($func, $args);
        }
    }
}

$foo = new Foo();
$foo->bar = function () { echo "Hello, this function is added at runtime"; };
$foo->bar();
karim79
quelle
6
Sehr, sehr klug, aber klobig in einem realen Projekt IMO! (und +1, um anonymen Downvoter entgegenzuwirken)
Pekka
2
@ Pekka - aber wie genau ist es kludgy? Sicher, ich sage nicht, dass ich ein Experte für das Erweitern eines Objekts zur Laufzeit in PHP bin, aber ich kann ehrlich gesagt nicht sagen, dass ich viel falsch daran sehe. (Vielleicht habe ich nur einen schlechten Geschmack)
Karim79
16
Ich würde auch einen is_callable-Check hinzufügen (wenn Sie also nicht versehentlich versuchen, einen String aufzurufen). Und werfen Sie auch eine BadMethodCallException (), wenn Sie eine Methode nicht finden können, sodass sie nicht zurückgegeben wird und Sie glauben, dass sie erfolgreich zurückgegeben wurde. Machen Sie auch den ersten Parameter der Funktion zum Objekt selbst und führen Sie dann array_unshift($args, $this);vor dem Methodenaufruf einen aus, damit die Funktion einen Verweis auf das Objekt erhält, ohne es explizit binden zu müssen ...
ircmaxell
3
Ich denke, die Leute verpassen den Punkt, die ursprüngliche Anforderung war die Verwendung eines klassenlosen Objekts.
Thomas-Peter
1
Ich würde tatsächlich vorschlagen, dass Sie den isset () - Test vollständig entfernen, da Sie, wenn diese Funktion nicht definiert ist, wahrscheinlich nicht stillschweigend zurückkehren möchten.
Alexis Wilke
50

Mit PHP 7 können Sie anonyme Klassen verwenden

$myObject = new class {
    public function myFunction(){}
};

$myObject->myFunction();

PHP RFC: Anonyme Klassen

Mohamed Ramrami
quelle
20

Die Verwendung von einfach __call, um das Hinzufügen neuer Methoden zur Laufzeit zu ermöglichen, hat den Hauptnachteil, dass diese Methoden die $ this-Instanzreferenz nicht verwenden können. Alles funktioniert hervorragend, bis die hinzugefügten Methoden $ this nicht im Code verwenden.

class AnObj extends stdClass
{
    public function __call($closure, $args)
    {
        return call_user_func_array($this->{$closure}, $args);
    }
 }
 $a=new AnObj();
 $a->color = "red";
 $a->sayhello = function(){ echo "hello!";};
 $a->printmycolor = function(){ echo $this->color;};
 $a->sayhello();//output: "hello!"
 $a->printmycolor();//ERROR: Undefined variable $this

Um dieses Problem zu lösen, können Sie das Muster auf diese Weise neu schreiben

class AnObj extends stdClass
{
    public function __call($closure, $args)
    {
        return call_user_func_array($this->{$closure}->bindTo($this),$args);
    }

    public function __toString()
    {
        return call_user_func($this->{"__toString"}->bindTo($this));
    }
}

Auf diese Weise können Sie neue Methoden hinzufügen, die die Instanzreferenz verwenden können

$a=new AnObj();
$a->color="red";
$a->sayhello = function(){ echo "hello!";};
$a->printmycolor = function(){ echo $this->color;};
$a->sayhello();//output: "hello!"
$a->printmycolor();//output: "red"
Alexroat
quelle
12

Update: Der hier gezeigte Ansatz weist ein großes Manko auf: Die neue Funktion ist kein voll qualifiziertes Mitglied der Klasse. $thisist in der Methode nicht vorhanden, wenn sie auf diese Weise aufgerufen wird. Dies bedeutet, dass Sie das Objekt als Parameter an die Funktion übergeben müssen, wenn Sie mit Daten oder Funktionen aus der Objektinstanz arbeiten möchten! Außerdem können Sie über diese Funktionen nicht auf privateoder protectedMitglieder der Klasse zugreifen .

Gute Frage und clevere Idee mit den neuen anonymen Funktionen!

Interessanterweise funktioniert dies: Ersetzen

$me->doSomething();    // Doesn't work

von call_user_func für die Funktion selbst :

call_user_func($me->doSomething);    // Works!

Was nicht funktioniert, ist der "richtige" Weg:

call_user_func(array($me, "doSomething"));   // Doesn't work

Wenn PHP so aufgerufen wird, muss die Methode in der Klassendefinition deklariert werden.

Ist das ein private/ public/ protectedSichtbarkeitsproblem?

Update: Nein. Es ist unmöglich, die Funktion auch innerhalb der Klasse auf normale Weise aufzurufen, daher ist dies kein Sichtbarkeitsproblem. call_user_func()Nur wenn ich die eigentliche Funktion an übergebe , kann ich scheinen, dass dies funktioniert.

Pekka
quelle
2

Sie können Funktionen auch in einem Array speichern:

<?php
class Foo
{
    private $arrayFuncs=array();// array of functions
    //
    public function addFunc($name,$function){
        $this->arrayFuncs[$name] = $function;
    }
    //
    public function callFunc($namefunc,$params=false){
        if(!isset($this->arrayFuncs[$namefunc])){
            return 'no function exist';
        }
        if(is_callable($this->arrayFuncs[$namefunc])){
            return call_user_func($this->arrayFuncs[$namefunc],$params);
        }
    }

}
$foo = new Foo();

//Save function on array variable with params
$foo->addFunc('my_function_call',function($params){
    return array('data1'=>$params['num1'],'data2'=>'qwerty','action'=>'add');
});
//Save function on array variable
$foo->addFunc('my_function_call2',function(){
    return 'a simple function';
});
//call func 1
$data = $foo->callFunc('my_function_call',array('num1'=>1224343545));
var_dump($data);
//call func 2
$data = $foo->callFunc('my_function_call2');
var_dump($data);
?>
Miglio
quelle
1

Um zu sehen, wie dies mit eval gemacht wird, können Sie sich mein PHP-Mikro-Framework Halcyon ansehen, das auf github verfügbar ist . Es ist klein genug, damit Sie es problemlos herausfinden können - konzentrieren Sie sich auf die HalcyonClassMunger-Klasse.

ivans
quelle
Ich gehe davon aus (und mein Code auch), dass Sie auf PHP <5.3.0 laufen und daher Kludges und Hacks benötigen, damit dies funktioniert.
Ivan
Die Frage, ob jemand die Ablehnung kommentieren möchte, ist wohl sinnlos ...
ivans
Es ist wahrscheinlich, dass hier einige Massenabstimmungen stattfinden. Aber vielleicht möchten Sie etwas näher auf das eingehen, was Sie getan haben? Wie Sie sehen, ist dies eine interessante Frage, auf die es noch keine eindeutige Antwort gibt.
Pekka
1

Ohne die __callLösung können Sie bindTo(PHP> = 5.4) verwenden, um die Methode mit $thisgebundenem Wert $mewie folgt aufzurufen :

call_user_func($me->doSomething->bindTo($me, null)); 

Das vollständige Skript könnte folgendermaßen aussehen:

$me = new stdClass;
// Property for proving that the method has access to the above object:
$me->message = "I\'ve done something"; 
$me->doSomething = function () {
    echo $this->message;
};
call_user_func($me->doSomething->bindTo($me)); // "I've done something"

Alternativ können Sie die gebundene Funktion einer Variablen zuweisen und sie dann aufrufen, ohne call_user_func:

$f = $me->doSomething->bindTo($me);
$f(); 
Trincot
quelle
0

Es gibt einen ähnlichen Beitrag zum Stapelüberlauf , der klarstellt , dass dies nur durch die Implementierung bestimmter Entwurfsmuster erreichbar ist.

Der einzige andere Weg ist die Verwendung von Classkit , einer experimentellen PHP-Erweiterung. (auch in der Post)

Ja, es ist möglich, einer PHP-Klasse eine Methode hinzuzufügen, nachdem sie definiert wurde. Sie möchten das Classkit verwenden, eine "experimentelle" Erweiterung. Es scheint jedoch, dass diese Erweiterung nicht standardmäßig aktiviert ist. Es hängt also davon ab, ob Sie eine benutzerdefinierte PHP-Binärdatei kompilieren oder unter Windows PHP-DLLs laden können (Dreamhost erlaubt beispielsweise benutzerdefinierte PHP-Binärdateien und sie sind recht einfach einzurichten ).

Zilverdistel
quelle
0

karim79 answer funktioniert, speichert jedoch anonyme Funktionen in Methodeneigenschaften und seine Deklarationen können vorhandene Eigenschaften mit demselben Namen überschreiben oder funktionieren nicht, wenn vorhandene Eigenschaften privateschwerwiegende Fehlerobjekte verursachen, die unbrauchbar sind. Ich denke, sie in einem separaten Array zu speichern und setter zu verwenden, ist eine sauberere Lösung. Der Methodeninjektorsetzer kann jedem Objekt automatisch mithilfe von Merkmalen hinzugefügt werden .

PS Natürlich ist dies ein Hack, der nicht verwendet werden sollte, da er gegen das Open-Closed-Prinzip von SOLID verstößt.

class MyClass {

    //create array to store all injected methods
    private $anon_methods = array();

    //create setter to fill array with injected methods on runtime
    public function inject_method($name, $method) {
        $this->anon_methods[$name] = $method;
    }

    //runs when calling non existent or private methods from object instance
    public function __call($name, $args) {
        if ( key_exists($name, $this->anon_methods) ) {
            call_user_func_array($this->anon_methods[$name], $args);
        }
    }

}


$MyClass = new MyClass;

//method one
$print_txt = function ($text) {
    echo $text . PHP_EOL;
};

$MyClass->inject_method("print_txt", $print_txt);

//method
$add_numbers = function ($n, $e) {
    echo "$n + $e = " . ($n + $e);
};

$MyClass->inject_method("add_numbers", $add_numbers);


//Use injected methods
$MyClass->print_txt("Hello World");
$MyClass->add_numbers(5, 10);
Roman Toasov
quelle
Wenn es sich um einen Hack handelt, der nicht verwendet werden sollte, sollte er nicht als Antwort veröffentlicht werden.
miken32