Como adicionar um novo método a um objeto php na hora?


98

Como adiciono um novo método a um objeto "em tempo real"?

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

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

Isso é especificamente sem usar uma classe?
thomas-peter

1
Você pode usar __call. Mas, por favor, não use __call. Alterar dinamicamente o comportamento de um objeto é uma maneira muito fácil de tornar seu código ilegível e impossível de manter.
GordonM

Respostas:


94

Você pode aproveitar __callpara isso:

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
Muito, muito inteligente, mas desajeitado em um projeto do mundo real IMO! (e +1 para contrariar o downvoter anônimo)
Pekka

2
@pekka - mas como, exatamente, é kludgy? Claro, não estou dizendo que sou um especialista em estender um objeto durante o tempo de execução em PHP, mas honestamente não posso dizer que vejo muito de errado nisso. (talvez eu só tenha mau gosto)
karim79

16
Eu adicionaria uma verificação is_callable nesse if também (para que você não tente acidentalmente chamar uma string). E também lance um BadMethodCallException () se você não conseguir encontrar um método, então você não o fez retornar e acha que ele retornou com sucesso. Além disso, faça do primeiro parâmetro da função o próprio objeto e, em seguida, faça um array_unshift($args, $this);antes da chamada do método, de modo que a função obtenha uma referência ao objeto sem a necessidade de vinculá-lo explicitamente ...
ircmaxell

3
Acho que as pessoas estão perdendo o ponto, o requisito original era usar um objeto classless.
thomas-peter

1
Na verdade, eu sugeriria que você remova completamente o teste isset () porque, se essa função não estiver definida, você provavelmente não deseja retornar silenciosamente.
Alexis Wilke


20

Usar simplesmente __call para permitir a adição de novos métodos em tempo de execução tem a principal desvantagem de que esses métodos não podem usar a referência $ this instância. Tudo funciona muito bem, até que os métodos adicionados não usem $ this no 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, você pode reescrever o padrão desta forma

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

Desta forma, você pode adicionar novos métodos que podem usar a referência de instância

$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

Atualização: a abordagem mostrada aqui tem uma grande lacuna: a nova função não é um membro totalmente qualificado da classe; $thisnão está presente no método quando chamado dessa maneira. Isso significa que você teria que passar o objeto para a função como um parâmetro se quiser trabalhar com dados ou funções da instância do objeto! Além disso, você não poderá acessar privateou protectedmembros da classe a partir dessas funções.

Boa pergunta e ideia inteligente usando as novas funções anônimas!

Curiosamente, isso funciona: Substitua

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

por call_user_func na própria função :

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

o que não funciona é a maneira "certa":

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

se chamado dessa forma, o PHP requer que o método seja declarado na definição da classe.

Isso é um private problema de visibilidade / public/ protected?

Atualização: Não. É impossível chamar a função da maneira normal mesmo de dentro da classe, portanto, esse não é um problema de visibilidade. Passar a função real para call_user_func()é a única maneira de fazer isso funcionar.


2

você também pode salvar funções em uma 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);
?>

1

Para ver como fazer isso com eval, você pode dar uma olhada no meu micro-framework PHP, Halcyon, que está disponível no github . É pequeno o suficiente para que você consiga descobrir sem problemas - concentre-se na classe HalcyonClassMunger.


Estou assumindo (e meu código também) que você está executando em PHP <5.3.0 e, portanto, precisa de kludges e hacks para fazer isso funcionar.
Ivans

Perguntar se alguém gostaria de comentar sobre o downvote é inútil, suponho ...
ivans

Provavelmente é, há alguns downvoting em massa por aqui. Mas talvez você queira elaborar um pouco sobre o que você fez? Como você pode ver, esta é uma pergunta interessante sem uma resposta definitiva ainda.
Pekka

1

Sem a __callsolução, você pode usar bindTo(PHP> = 5.4), para chamar o método com $thislimite da $meseguinte maneira:

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

O script completo pode ter a seguinte aparência:

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

Como alternativa, você pode atribuir a função vinculada a uma variável e chamá-la sem call_user_func:

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

0

Há uma postagem semelhante sobre stackoverflow que esclarece que isso só é possível por meio da implementação de certos padrões de design.

A única outra maneira é através do uso de classkit , uma extensão experimental de php. (também na postagem)

Sim, é possível adicionar um método a uma classe PHP após sua definição. Você deseja usar o classkit, que é uma extensão "experimental". Parece que esta extensão não está habilitada por padrão, portanto, depende se você pode compilar um binário PHP personalizado ou carregar DLLs PHP se estiver no Windows (por exemplo, o Dreamhost permite binários PHP personalizados e eles são muito fáceis de configurar )


0

A resposta karim79 funciona, mas armazena funções anônimas dentro das propriedades do método e suas declarações podem sobrescrever as propriedades existentes com o mesmo nome ou não funcionam se a propriedade existente está privatecausando erro fatal. Objeto inutilizável. Acho que armazená-los em um array separado e usar setter é uma solução mais limpa. O método injetor setter pode ser adicionado a cada objeto automaticamente usando traits .

PS Claro que este é um hack que não deve ser usado, pois viola o princípio aberto fechado 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);

Se for um hack que não deve ser usado, não deve ser postado como uma resposta.
miken32,
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.