PHP __get e __set métodos mágicos

86

A menos que eu esteja completamente enganado, os métodos __gete __setdevem permitir a sobrecarga do → gete set.

Por exemplo, as seguintes instruções devem invocar o __getmétodo:

echo $foo->bar;
$var = $foo->bar;

E o seguinte deve usar o __setmétodo:

$foo->bar = 'test';

Isso não estava funcionando em meu código e é reproduzível com este exemplo simples:

class foo {

    public $bar;
    public function __get($name) {

        echo "Get:$name";
        return $this->$name;
    }

    public function __set($name, $value) {

        echo "Set:$name to $value";
        $this->$name = $value;
    }
}


$foo = new foo();

echo $foo->bar;
$foo->bar = 'test';

echo "[$foo->bar]";

Isso só resulta em:

[test]

Fazer algumas die()ligações mostra que não está acertando nada.

Por enquanto, eu apenas disse dane-se e estou usando manualmente __getonde é necessário por enquanto, mas isso não é muito dinâmico e requer conhecimento de que o código 'sobrecarregado' na verdade não está sendo chamado a menos que seja especificamente chamado. Gostaria de saber se isso não deve funcionar da maneira que entendi que deveria ou por que não está funcionando.

Isso está funcionando php 5.3.3.

urso aéreo
fonte

Respostas:

166

__get, __set, __callE __callStaticsão chamados quando o método ou propriedade é inacessível. Seu $baré público e, portanto, não está inacessível.

Consulte a seção sobre sobrecarga de propriedade no manual:

  • __set() é executado ao gravar dados em propriedades inacessíveis.
  • __get() é utilizado para ler dados de propriedades inacessíveis.

Os métodos mágicos não são substitutos para getters e setters. Eles apenas permitem que você lide com chamadas de método ou acesso à propriedade que, de outra forma, resultaria em um erro. Como tal, há muito mais coisas relacionadas ao tratamento de erros. Observe também que eles são consideravelmente mais lentos do que usar getter e setter adequados ou chamadas de método diretas.

Gordon
fonte
6
Para elaborar sobre isso, basta remover "public $ bar" para que a propriedade não exista mais e funcionará como um encanto.
Steffen Müller
Obrigado. Isso é o que eu ganho por não ler completamente o manual e, em vez disso, olhar alguns exemplos do blog de alguns caras;) No entanto, ainda estou esperando por uma verdadeira sobrecarga de operadores em PHP.
airbear
@airbear há um pacote PECL antigo de Sara Golemon que permitiria a você sobrecarregar os operadores . Não sei o quão compatível com PHP.current ele é.
Gordon
1
@Pooya Isso porque nodenão é uma propriedade de $ foo, mas sim de doesNotExist. Portanto, a menos que 'doesNotExist' seja um objeto (que implementa __set ou tenha uma propriedade pública chamada node), ele não funcionará.
Tivie
1
@Allen sim, é verdade.
Gordon
34

Eu recomendo usar uma matriz para armazenar todos os valores via __set().

class foo {

    protected $values = array();

    public function __get( $key )
    {
        return $this->values[ $key ];
    }

    public function __set( $key, $value )
    {
        $this->values[ $key ] = $value;
    }

}

Desta forma, você se certifica de que não pode acessar as variáveis ​​de outra maneira (note que $valuesestá protegida), para evitar colisões.

Fidi
fonte
19

Do manual do PHP :

  • __set () é executado ao gravar dados em propriedades inacessíveis.
  • __get () é utilizado para ler dados de propriedades inacessíveis.

Isso só é chamado na leitura / gravação de propriedades inacessíveis . Sua propriedade, entretanto, é pública, o que significa que está acessível. Alterar o modificador de acesso para protegido resolve o problema.

Berry Langerak
fonte
7

Para expandir a resposta de Berry, que definir o nível de acesso como protegido permite que __get e __set sejam usados ​​com propriedades declaradas explicitamente (quando acessados ​​fora da classe, pelo menos) e a velocidade sendo consideravelmente mais lenta, citarei um comentário de outra pergunta sobre este tópico e tente usá-lo de qualquer maneira:

Eu concordo que __get é mais lento para uma função get customizada (fazendo as mesmas coisas), isto é 0,0124455 o tempo para __get () e 0,0024445 é para get () customizado após 10.000 loops. - Melsi 23 de novembro de 12 às 22:32 Prática recomendada: métodos mágicos de PHP __set e __get

De acordo com os testes de Melsi, consideravelmente mais lento é cerca de 5 vezes mais lento. Isso é definitivamente consideravelmente mais lento, mas observe também que os testes mostram que você ainda pode acessar uma propriedade com esse método 10.000 vezes, contando o tempo para a iteração do loop, em cerca de 1/100 de segundo. É consideravelmente mais lento em comparação com os métodos get e set reais definidos, e isso é um eufemismo, mas no grande esquema das coisas, mesmo 5 vezes mais lento nunca é realmente lento.

O tempo de computação da operação ainda é insignificante e não vale a pena ser considerado em 99% dos aplicativos do mundo real. A única vez que isso realmente deve ser evitado é quando você realmente vai acessar as propriedades mais de 10.000 vezes em uma única solicitação. Os sites de alto tráfego estão fazendo algo realmente errado se não podem arcar com o aumento de alguns servidores para manter seus aplicativos em execução. Um anúncio de texto de uma única linha no rodapé de um site de alto tráfego, onde a taxa de acesso se torna um problema, provavelmente pagaria por um conjunto de 1.000 servidores com essa linha de texto. O usuário final nunca vai ficar batendo os dedos se perguntando por que a página está demorando tanto para carregar porque o acesso à propriedade do seu aplicativo leva um milionésimo de segundo.

Digo isso falando como um desenvolvedor vindo de .NET, mas os métodos invisíveis get e set para o consumidor não são invenção do .NET. Eles simplesmente não são propriedades sem eles, e esses métodos mágicos são a graça salvadora do desenvolvedor do PHP por até mesmo chamar sua versão de propriedades de "propriedades". Além disso, a extensão do Visual Studio para PHP oferece suporte a intellisense com propriedades protegidas, com esse truque em mente, eu acho. Eu acho que com desenvolvedores suficientes usando os métodos mágicos __get e __set dessa forma, os desenvolvedores de PHP ajustariam o tempo de execução para atender à comunidade de desenvolvedores.

Edit: Em teoria, as propriedades protegidas pareciam funcionar na maioria das situações. Na prática, acontece que muitas vezes você vai querer usar seus getters e setters ao acessar propriedades dentro da definição de classe e classes estendidas. Uma solução melhor é uma classe base e interface para quando estender outras classes, portanto, você pode apenas copiar algumas linhas de código da classe base para a classe de implementação. Estou fazendo um pouco mais com a classe base do meu projeto, então não tenho uma interface para fornecer agora, mas aqui está a definição de classe reduzida não testada com propriedade mágica obtendo e configurando usando reflexão para remover e mover as propriedades para uma matriz protegida:

/** Base class with magic property __get() and __set() support for defined properties. */
class Component {
    /** Gets the properties of the class stored after removing the original
     * definitions to trigger magic __get() and __set() methods when accessed. */
    protected $properties = array();

    /** Provides property get support. Add a case for the property name to
     * expand (no break;) or replace (break;) the default get method. When
     * overriding, call parent::__get($name) first and return if not null,
     * then be sure to check that the property is in the overriding class
     * before doing anything, and to implement the default get routine. */
    public function __get($name) {
        $caller = array_shift(debug_backtrace());
        $max_access = ReflectionProperty::IS_PUBLIC;
        if (is_subclass_of($caller['class'], get_class($this)))
            $max_access = ReflectionProperty::IS_PROTECTED;
        if ($caller['class'] == get_class($this))
            $max_access = ReflectionProperty::IS_PRIVATE;
        if (!empty($this->properties[$name])
            && $this->properties[$name]->class == get_class()
            && $this->properties[$name]->access <= $max_access)
            switch ($name) {
                default:
                    return $this->properties[$name]->value;
            }
    }

    /** Provides property set support. Add a case for the property name to
     * expand (no break;) or replace (break;) the default set method. When
     * overriding, call parent::__set($name, $value) first, then be sure to
     * check that the property is in the overriding class before doing anything,
     * and to implement the default set routine. */
    public function __set($name, $value) {
        $caller = array_shift(debug_backtrace());
        $max_access = ReflectionProperty::IS_PUBLIC;
        if (is_subclass_of($caller['class'], get_class($this)))
            $max_access = ReflectionProperty::IS_PROTECTED;
        if ($caller['class'] == get_class($this))
            $max_access = ReflectionProperty::IS_PRIVATE;
        if (!empty($this->properties[$name])
            && $this->properties[$name]->class == get_class()
            && $this->properties[$name]->access <= $max_access)
            switch ($name) {
                default:
                    $this->properties[$name]->value = $value;
            }
    }

    /** Constructor for the Component. Call first when overriding. */
    function __construct() {
        // Removing and moving properties to $properties property for magic
        // __get() and __set() support.
        $reflected_class = new ReflectionClass($this);
        $properties = array();
        foreach ($reflected_class->getProperties() as $property) {
            if ($property->isStatic()) { continue; }
            $properties[$property->name] = (object)array(
                'name' => $property->name, 'value' => $property->value
                , 'access' => $property->getModifier(), 'class' => get_class($this));
            unset($this->{$property->name}); }
        $this->properties = $properties;
    }
}

Minhas desculpas se houver algum bug no código.

Jason Ensinger
fonte
Encontrei um problema em 'access' => $ property-> getModifier ()
macki
5

É porque $ bar é uma propriedade pública.

$foo->bar = 'test';

Não há necessidade de chamar o método mágico ao executar o acima.

Excluir public $bar;da sua classe deve corrigir isso.

Matt Lowden
fonte
1

É melhor usar métodos mágicos de definir / obter com métodos de definir / obter personalizados predefinidos como no exemplo abaixo. Desta forma, você pode combinar o melhor de dois mundos. Em termos de velocidade concordo que são um pouco mais lentos, mas dá para sentir a diferença. O exemplo abaixo também valida a matriz de dados em relação aos configuradores predefinidos.

"Os métodos mágicos não são substitutos para getters e setters. Eles apenas permitem que você lide com chamadas de métodos ou acesso a propriedades que, de outra forma, resultariam em um erro."

É por isso que devemos usar ambos.

EXEMPLO DE ITEM DE CLASSE

    /*
    * Item class
    */
class Item{
    private $data = array();

    function __construct($options=""){ //set default to none
        $this->setNewDataClass($options); //calling function
    }

    private function setNewDataClass($options){
        foreach ($options as $key => $value) {
            $method = 'set'.ucfirst($key); //capitalize first letter of the key to preserve camel case convention naming
            if(is_callable(array($this, $method))){  //use seters setMethod() to set value for this data[key];      
                $this->$method($value); //execute the setters function
            }else{
                $this->data[$key] = $value; //create new set data[key] = value without seeters;
            }   
        }
    }

    private function setNameOfTheItem($value){ // no filter
        $this->data['name'] = strtoupper($value); //assign the value
        return $this->data['name']; // return the value - optional
    }

    private function setWeight($value){ //use some kind of filter
        if($value >= "100"){ 
            $value = "this item is too heavy - sorry - exceeded weight of maximum 99 kg [setters filter]";
        }
        $this->data['weight'] = strtoupper($value); //asign the value
        return $this->data['weight']; // return the value - optional
    }

    function __set($key, $value){
        $method = 'set'.ucfirst($key); //capitalize first letter of the key to preserv camell case convention naming
        if(is_callable(array($this, $method))){  //use seters setMethod() to set value for this data[key];      
            $this->$method($value); //execute the seeter function
        }else{
            $this->data[$key] = $value; //create new set data[key] = value without seeters;
        }
    }

    function __get($key){
        return $this->data[$key];
    }

    function dump(){
        var_dump($this);
    }
}

INDEX.PHP

$data = array(
    'nameOfTheItem' => 'tv',
    'weight' => '1000',
    'size' => '10x20x30'
);

$item = new Item($data);
$item->dump();

$item->somethingThatDoNotExists = 0; // this key (key, value) will trigger magic function __set() without any control or check of the input,
$item->weight = 99; // this key will trigger predefined setter function of a class - setWeight($value) - value is valid,
$item->dump();

$item->weight = 111; // this key will trigger predefined setter function of a class - setWeight($value) - value invalid - will generate warning.
$item->dump(); // display object info

RESULTADO

object(Item)[1]
  private 'data' => 
    array (size=3)
      'name' => string 'TV' (length=2)
      'weight' => string 'THIS ITEM IS TOO HEAVY - SORRY - EXIDED WEIGHT OF MAXIMUM 99 KG [SETTERS FILTER]' (length=80)
      'size' => string '10x20x30' (length=8)
object(Item)[1]
  private 'data' => 
    array (size=4)
      'name' => string 'TV' (length=2)
      'weight' => string '99' (length=2)
      'size' => string '10x20x30' (length=8)
      'somethingThatDoNotExists' => int 0
object(Item)[1]
  private 'data' => 
    array (size=4)
      'name' => string 'TV' (length=2)
      'weight' => string 'THIS ITEM IS TOO HEAVY - SORRY - EXIDED WEIGHT OF MAXIMUM 99 KG [SETTERS FILTER]' (length=80)
      'size' => string '10x20x30' (length=8)
      'somethingThatDoNotExists' => int 0
DevWL
fonte
0

Cancele a public $bar;declaração e ela deve funcionar conforme o esperado.

Alix Axel
fonte
-6

Intenta con:

__GET($k){
 return $this->$k;
}

_SET($k,$v){
 return $this->$k = $v;
}
charly
fonte