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.