Come aggiungere un nuovo metodo a un oggetto php al volo?


98

Come si aggiunge un nuovo metodo a un oggetto "al volo"?

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

//Fatal error: Call to undefined method stdClass::doSomething()

È questo specificamente senza usare una classe?
thomas-peter

1
Potresti usare __call. Ma per favore non usare __call. Modificare dinamicamente il comportamento di un oggetto è un modo molto semplice per rendere il codice illeggibile e non mantenibile.
GordonM

Risposte:


94

Puoi sfruttare __callper questo:

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();

6
Molto, molto intelligente ma kludgy in un progetto del mondo reale IMO! (e +1 per contrastare il downvoter anonimo)
Pekka

2
@pekka - ma come, esattamente, è kludgy? Certo, non sto dicendo di essere un esperto nell'estensione di un oggetto durante il runtime in PHP, ma onestamente non posso dire di vederlo molto sbagliato. (forse ho solo cattivo gusto)
karim79

16
Aggiungerei anche un controllo is_callable se (quindi non provi accidentalmente a chiamare una stringa). E lancia anche un'eccezione BadMethodCallException () se non riesci a trovare un metodo, quindi non lo fai restituire e pensi che sia tornato correttamente. Inoltre, rendi il primo parametro della funzione l'oggetto stesso, quindi esegui una array_unshift($args, $this);chiamata prima della chiamata al metodo, in modo che la funzione ottenga un riferimento all'oggetto senza bisogno esplicitamente di vincolarlo ...
ircmaxell

3
Penso che le persone non capiscano il punto, il requisito originale era l'utilizzo di un oggetto senza classi.
thomas-peter

1
In realtà ti suggerirei di rimuovere completamente il test isset () perché se quella funzione non è definita, probabilmente non vuoi tornare silenziosamente.
Alexis Wilke


20

Usare semplicemente __call per consentire l'aggiunta di nuovi metodi in fase di esecuzione ha il principale svantaggio che questi metodi non possono usare il riferimento $ this instance. Tutto funziona alla grande, fino a quando i metodi aggiunti non usano $ this nel codice.

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

Per risolvere questo problema puoi riscrivere il pattern in questo modo

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));
    }
}

In questo modo puoi aggiungere nuovi metodi che possono utilizzare il riferimento all'istanza

$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"

12

Aggiornamento: l'approccio mostrato qui ha un grave difetto: la nuova funzione non è un membro pienamente qualificato della classe; $thisnon è presente nel metodo quando viene richiamato in questo modo. Ciò significa che dovresti passare l'oggetto alla funzione come parametro se vuoi lavorare con dati o funzioni dall'istanza dell'oggetto! Inoltre, non sarà in grado di accedere privateo di protectedmembri della classe da queste funzioni.

Bella domanda e idea intelligente utilizzando le nuove funzioni anonime!

È interessante notare che funziona: sostituire

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

da call_user_func sulla funzione stessa :

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

quello che non funziona è il modo "giusto":

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

se chiamato in questo modo, PHP richiede che il metodo venga dichiarato nella definizione della classe.

È un problema di visibilità private/ public/ protected?

Aggiornamento: no. È impossibile chiamare la funzione nel modo normale anche dall'interno della classe, quindi questo non è un problema di visibilità. Passare la funzione effettiva a call_user_func()è l'unico modo in cui riesco a farlo funzionare.


2

puoi anche salvare le funzioni in un array:

<?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);
?>

1

Per vedere come farlo con eval, puoi dare un'occhiata al mio micro-framework PHP, Halcyon, che è disponibile su GitHub . È abbastanza piccolo da essere in grado di capirlo senza problemi: concentrati sulla classe HalcyonClassMunger.


Presumo (e così anche il mio codice) che tu stia utilizzando PHP <5.3.0 e quindi abbia bisogno di kludge e hack per farlo funzionare.
ivans

Chiedere se qualcuno volesse commentare il
voto negativo

Probabilmente lo è, c'è un voto negativo di massa qui intorno. Ma forse vuoi approfondire un po 'quello che hai fatto? Come puoi vedere, questa è una domanda interessante senza ancora una risposta definitiva.
Pekka

1

Senza la __callsoluzione, puoi usare bindTo(PHP> = 5.4), per chiamare il metodo con $thisassociato in $mequesto modo:

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

Lo script completo potrebbe assomigliare a questo:

$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"

In alternativa, puoi assegnare la funzione associata a una variabile e quindi chiamarla senza call_user_func:

$f = $me->doSomething->bindTo($me);
$f(); 

0

C'è un post simile su stackoverflow che chiarisce che questo è possibile solo attraverso l'implementazione di determinati modelli di progettazione.

L'unico altro modo è attraverso l'uso di classkit , un'estensione php sperimentale. (anche nel post)

Sì, è possibile aggiungere un metodo a una classe PHP dopo che è stata definita. Vuoi usare classkit, che è un'estensione "sperimentale". Sembra che questa estensione non sia abilitata per impostazione predefinita, quindi dipende dalla possibilità di compilare un binario PHP personalizzato o caricare DLL PHP se su Windows (ad esempio Dreamhost consente binari PHP personalizzati e sono piuttosto facili da configurare ).


0

La risposta di karim79 funziona tuttavia memorizza le funzioni anonime all'interno delle proprietà del metodo e le sue dichiarazioni possono sovrascrivere le proprietà esistenti con lo stesso nome o non funziona se la proprietà esistente privatecausa un errore fatale oggetto inutilizzabile, penso che memorizzarle in un array separato e usare setter sia una soluzione più pulita. Il setter dell'iniettore del metodo può essere aggiunto automaticamente a ogni oggetto utilizzando i tratti .

PS Ovviamente questo è un trucco che non dovrebbe essere usato in quanto viola il principio aperto chiuso di SOLID.

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);

Se si tratta di un hack che non dovrebbe essere utilizzato, non dovrebbe essere pubblicato come risposta.
miken32
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.