Métodos mágicos PHP __get y __set

85

A menos que esté completamente equivocado, se supone que los métodos __gety __setpermiten la sobrecarga de → gety set.

Por ejemplo, las siguientes declaraciones deben invocar el __getmétodo:

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

Y lo siguiente debería usar el __setmétodo:

$foo->bar = 'test';

Esto no funcionaba en mi código y es reproducible con este simple ejemplo:

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]";

Esto solo da como resultado:

[test]

Poner algunas die()llamadas allí muestra que no lo está haciendo en absoluto.

Por ahora, acabo de decir que se joda, y estoy usando manualmente __getdonde se necesita por ahora, pero eso no es muy dinámico y requiere conocimiento de que el código 'sobrecargado' de hecho no se llama a menos que se llame específicamente. Me gustaría saber si se supone que esto no funciona de la manera que he entendido que debería o por qué no funciona.

Esto sigue funcionando php 5.3.3.

airbear
fuente

Respuestas:

164

__get, __set, __callY __callStaticson invocados cuando el método o propiedad es inaccesible. Tu $bares público y por lo tanto no inaccesible.

Consulte la sección sobre sobrecarga de propiedades en el manual:

  • __set() se ejecuta al escribir datos en propiedades inaccesibles.
  • __get() se utiliza para leer datos de propiedades inaccesibles.

Los métodos mágicos no sustituyen a los getters y setters. Solo le permiten manejar llamadas a métodos o acceso a propiedades que de otra manera resultarían en un error. Como tal, hay mucho más relacionado con el manejo de errores. También tenga en cuenta que son considerablemente más lentos que usar un getter y setter adecuado o llamadas a métodos directos.

Gordon
fuente
5
Para profundizar en esto, simplemente elimine "public $ bar" para que la propiedad ya no exista y funcione como un encanto.
Steffen Müller
Gracias. Eso es lo que obtengo por no leer detenidamente el manual y, en cambio, ver algunos ejemplos de blogs de chicos;) Sin embargo, todavía estoy esperando una verdadera sobrecarga del operador en PHP.
airbear
@airbear hay un antiguo paquete PECL de Sara Golemon que le permitiría sobrecargar operadores . Sin embargo, no sé qué tan compatible con PHP.current es.
Gordon
1
@Pooya Eso es porque nodeno es una propiedad de $ foo sino una propiedad de doesNotExist. Entonces, a menos que 'doesNotExist' sea un objeto (que implemente __set o tenga una propiedad pública llamada nodo), no funcionará.
Tivie
1
@ Allen sí, lo hace.
Gordon
34

Recomendaría usar una matriz para almacenar todos los valores a través de __set().

class foo {

    protected $values = array();

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

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

}

De esta forma te aseguras de no poder acceder a las variables de otra forma (ten en cuenta que $valuesestá protegido), para evitar colisiones.

Fidi
fuente
19

Del manual de PHP :

  • __set () se ejecuta al escribir datos en propiedades inaccesibles.
  • __get () se utiliza para leer datos de propiedades inaccesibles.

Esto solo se solicita en propiedades inaccesibles de lectura / escritura . Sin embargo, su propiedad es pública, lo que significa que es accesible. Cambiar el modificador de acceso a protegido resuelve el problema.

Berry Langerak
fuente
7

Para ampliar la respuesta de Berry, que establecer el nivel de acceso en protected permite que __get y __set se usen con propiedades declaradas explícitamente (cuando se accede fuera de la clase, al menos) y la velocidad es considerablemente más lenta, citaré un comentario de otra pregunta sobre este tema y exponga su uso de todos modos:

Estoy de acuerdo en que __get es más lento para una función get personalizada (haciendo las mismas cosas), este es 0.0124455 el tiempo para __get () y este 0.0024445 es para get () personalizado después de 10000 bucles. - Melsi 23 de noviembre de 2012 a las 22:32 Mejores prácticas: Métodos PHP Magic __set y __get

Según las pruebas de Melsi, considerablemente más lento es aproximadamente 5 veces más lento. Eso es definitivamente considerablemente más lento, pero también tenga en cuenta que las pruebas muestran que aún puede acceder a una propiedad con este método 10,000 veces, contando el tiempo para la iteración del ciclo, en aproximadamente 1/100 de segundo. Es considerablemente más lento en comparación con los métodos get y set definidos, y eso es quedarse corto, pero en el gran esquema de las cosas, incluso 5 veces más lento nunca es realmente lento.

El tiempo de cálculo de la operación sigue siendo insignificante y no vale la pena considerarlo en el 99% de las aplicaciones del mundo real. El único momento en que realmente debería evitarse es cuando en realidad va a acceder a las propiedades más de 10,000 veces en una sola solicitud. Los sitios de alto tráfico están haciendo algo realmente mal si no pueden permitirse lanzar algunos servidores más para mantener sus aplicaciones en funcionamiento. Un anuncio de texto de una sola línea en el pie de página de un sitio de alto tráfico donde la tasa de acceso se convierte en un problema probablemente podría pagar por una granja de 1,000 servidores con esa línea de texto. El usuario final nunca va a tocar sus dedos preguntándose qué está tardando tanto en cargar la página porque el acceso a la propiedad de su aplicación tarda una millonésima de segundo.

Digo esto hablando como un desarrollador con experiencia en .NET, pero los métodos invisibles de obtención y configuración para el consumidor no son una invención de .NET. Simplemente no son propiedades sin ellos, y estos métodos mágicos son la gracia salvadora del desarrollador de PHP para incluso llamar "propiedades" a su versión de propiedades. Además, la extensión de Visual Studio para PHP admite intellisense con propiedades protegidas, con ese truco en mente, creo. Creo que con suficientes desarrolladores usando los métodos mágicos __get y __set de esta manera, los desarrolladores de PHP ajustarían el tiempo de ejecución para atender a la comunidad de desarrolladores.

Editar: En teoría, las propiedades protegidas parecían funcionar en la mayoría de las situaciones. En la práctica, resulta que muchas veces querrá utilizar sus captadores y definidores al acceder a propiedades dentro de la definición de clase y las clases extendidas. Una mejor solución es una clase base y una interfaz para cuando se extienden otras clases, por lo que puede copiar las pocas líneas de código de la clase base a la clase de implementación. Estoy haciendo un poco más con la clase base de mi proyecto, por lo que no tengo una interfaz para proporcionar en este momento, pero aquí está la definición de clase simplificada sin probar con la propiedad mágica obteniendo y configurando usando la reflexión para eliminar y mover las propiedades a una 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;
    }
}

Mis disculpas si hay algún error en el código.

Jason Ensinger
fuente
Encontré un problema en el 'acceso' => $ propiedad-> getModifier ()
macki
5

Es porque $ bar es una propiedad pública.

$foo->bar = 'test';

No es necesario llamar al método mágico cuando se ejecuta lo anterior.

Eliminar public $bar;de tu clase debería corregir esto.

Matt Lowden
fuente
1

Utilice mejor los métodos de configuración / obtención mágicos con métodos predefinidos de configuración / obtención personalizados como en el ejemplo siguiente. De esta forma puedes combinar lo mejor de dos mundos. En términos de velocidad, estoy de acuerdo en que son un poco más lentos, pero ¿puedes sentir la diferencia? El siguiente ejemplo también valida la matriz de datos con los establecedores predefinidos.

"Los métodos mágicos no sustituyen a los captadores y definidores. Solo le permiten manejar llamadas a métodos o acceso a propiedades que de otra manera resultarían en un error".

Es por eso que debemos usar ambos.

EJEMPLO DE ARTÍCULO DE CLASE

    /*
    * 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

SALIDA

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
fuente
0

Suelta la public $bar;declaración y debería funcionar como se esperaba.

Alix Axel
fuente
-6

Intenta con:

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

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