¿Cómo agregar un nuevo método a un objeto php sobre la marcha?

98

¿Cómo agrego un nuevo método a un objeto "sobre la marcha"?

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

//Fatal error: Call to undefined method stdClass::doSomething()
MadBad
fuente
¿Es esto específicamente sin usar una clase?
thomas-peter
1
Podrías usar __call. Pero no utilices __call. Cambiar dinámicamente el comportamiento de un objeto es una forma muy fácil de hacer que su código sea ilegible y no se pueda mantener.
GordonM
Puede utilizar esta herramienta: packagist.org/packages/drupol/dynamicobjects
Pol Dellaiera

Respuestas:

94

Puede aprovechar __callpara esto:

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
fuente
6
Muy, muy inteligente, pero torpe en un proyecto del mundo real, en mi opinión. (y +1 para contrarrestar votantes negativos anónimos)
Pekka
2
@pekka - pero ¿cómo, exactamente, es kludgy? Claro, no estoy diciendo que sea un experto en extender un objeto durante el tiempo de ejecución en PHP, pero honestamente no puedo decir que vea mucho mal en él. (tal vez solo tengo mal gusto)
karim79
16
También agregaría una verificación is_callable en ese caso (para que no intente llamar accidentalmente a una cadena). Y también arroje una BadMethodCallException () si no puede encontrar un método, por lo que no lo devuelve y cree que regresó con éxito. Además, convierta el primer parámetro de la función en el objeto en sí, y luego haga una array_unshift($args, $this);antes de la llamada al método, de modo que la función obtenga una referencia al objeto sin necesidad explícita de vincularlo ...
ircmaxell
3
Creo que la gente está perdiendo el punto, el requisito original era usar un objeto sin clases.
thomas-peter
1
De hecho, sugeriría que elimine por completo la prueba isset () porque si esa función no está definida, probablemente no quiera regresar en silencio.
Alexis Wilke
50

Con PHP 7 puedes usar clases anónimas

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

$myObject->myFunction();

PHP RFC: clases anónimas

Mohamed Ramrami
fuente
20

Usar simplemente __call para permitir agregar nuevos métodos en tiempo de ejecución tiene el mayor inconveniente de que esos métodos no pueden usar la referencia de instancia $ this. Todo funciona muy bien, hasta que los métodos agregados no usen $ this en el código.

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

Para resolver este problema, puede reescribir el patrón de esta manera

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

De esta manera, puede agregar nuevos métodos que pueden usar la referencia de instancia

$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
fuente
12

Actualización: el enfoque que se muestra aquí tiene una deficiencia importante: la nueva función no es un miembro completamente calificado de la clase; $thisno está presente en el método cuando se invoca de esta manera. Esto significa que tendría que pasar el objeto a la función como parámetro si desea trabajar con datos o funciones de la instancia del objeto. Además, usted no será capaz de acceder privateo de protectedlos miembros de la clase a partir de estas funciones.

¡Buena pregunta e inteligente idea usando las nuevas funciones anónimas!

Curiosamente, esto funciona: Reemplazar

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

por call_user_func en la función en sí :

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

lo que no funciona es la forma "correcta":

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

si se llama de esa manera, PHP requiere que el método se declare en la definición de la clase.

¿Es esta una private/ public/ protectedtema visibilidad?

Actualización: No. Es imposible llamar a la función de la forma normal incluso desde dentro de la clase, por lo que esto no es un problema de visibilidad. Pasar la función real a call_user_func()es la única forma en que puedo hacer que esto funcione.

Pekka
fuente
2

también puede guardar funciones en una matriz:

<?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
fuente
1

Para ver cómo hacer esto con eval, puede echar un vistazo a mi micro-framework PHP, Halcyon, que está disponible en github . Es lo suficientemente pequeño como para poder resolverlo sin ningún problema: concéntrese en la clase HalcyonClassMunger.

Ivans
fuente
Supongo (y también mi código) que está ejecutando PHP <5.3.0 y, por lo tanto, necesita trucos y trucos para que esto funcione.
ivans
Preguntar si alguien quisiera comentar sobre el
voto negativo
Probablemente lo sea, hay algunas votaciones negativas masivas por aquí. ¿Pero tal vez quieras desarrollar un poco lo que hiciste? Como puede ver, esta es una pregunta interesante sin una respuesta definitiva todavía.
Pekka
1

Sin la __callsolución, puede usar bindTo(PHP> = 5.4), para llamar al método con un $thislímite $measí:

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

El guión completo podría verse así:

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

Alternativamente, puede asignar la función vinculada a una variable y luego llamarla sin call_user_func:

$f = $me->doSomething->bindTo($me);
$f(); 
trincot
fuente
0

Hay una publicación similar en stackoverflow que aclara que esto solo se puede lograr mediante la implementación de ciertos patrones de diseño.

La única otra forma es mediante el uso de classkit , una extensión experimental de php. (también en la publicación)

Sí, es posible agregar un método a una clase PHP después de su definición. Quieres usar classkit, que es una extensión "experimental". Sin embargo, parece que esta extensión no está habilitada de forma predeterminada, por lo que depende de si puede compilar un binario PHP personalizado o cargar archivos DLL PHP si está en Windows (por ejemplo, Dreamhost permite binarios PHP personalizados, y son bastante fáciles de configurar ).

Zilverdistel
fuente
0

La respuesta karim79 funciona, sin embargo, almacena funciones anónimas dentro de las propiedades del método y sus declaraciones pueden sobrescribir las propiedades existentes del mismo nombre o no funciona si la propiedad existente está privatecausando un error fatal en el objeto inutilizable.Creo que almacenarlos en una matriz separada y usar setter es una solución más limpia. El setter inyector de método se puede agregar a cada objeto automáticamente usando rasgos .

PD: Por supuesto, este es un truco que no debe usarse ya que viola el principio abierto cerrado de 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);
Roman Toasov
fuente
Si es un truco que no debería usarse, no debería publicarse como respuesta.
miken32