Metody magiczne PHP __get i __set

85

O ile całkowicie się nie mylę, metody __geti __setmają pozwolić na przeciążenie → geti set.

Na przykład poniższe instrukcje powinny wywołać __getmetodę:

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

Z __setmetody tej powinny skorzystać :

$foo->bar = 'test';

To nie działało w moim kodzie i można je odtworzyć za pomocą tego prostego przykładu:

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

Skutkuje to tylko:

[test]

Umieszczenie tam niektórych die()połączeń pokazuje, że w ogóle nie trafia.

Na razie powiedziałem tylko „wkręć” i używam ręcznie __gettam, gdzie jest to potrzebne, ale nie jest to zbyt dynamiczne i wymaga wiedzy, że „przeciążony” kod w rzeczywistości nie jest wywoływany, chyba że zostanie specjalnie wywołany. Chciałbym wiedzieć, czy to albo nie powinno działać tak, jak zrozumiałem, lub dlaczego nie działa.

To trwa php 5.3.3.

airbear
źródło

Odpowiedzi:

164

__get, __set, __callI __callStaticsą wywoływane kiedy metoda lub właściwość jest niedostępne. Twój $barjest publiczny i dlatego nie jest niedostępny.

Zobacz sekcję dotyczącą przeciążania właściwości w podręczniku:

  • __set() jest uruchamiany podczas zapisywania danych do niedostępnych właściwości.
  • __get() służy do odczytywania danych z niedostępnych nieruchomości.

Metody magiczne nie zastępują metod pobierających i ustawiających. Umożliwiają one tylko obsługę wywołań metod lub dostępu do właściwości, które w przeciwnym razie spowodowałyby błąd. W związku z tym jest znacznie więcej problemów związanych z obsługą błędów. Zauważ również, że są one znacznie wolniejsze niż użycie odpowiedniego gettera i settera lub bezpośrednich wywołań metod.

Gordon
źródło
5
Aby to rozwinąć, po prostu usuń „public $ bar”, aby ta właściwość już nie istniała i będzie działać jak urok.
Steffen Müller
Dzięki. Właśnie to dostaję za to, że nie czytałem dokładnie instrukcji i zamiast tego patrzyłem na przykładowy blog niektórych facetów;) Jednak wciąż czekam na prawdziwe przeciążenie operatorów w PHP.
airbear
@airbear jest stary pakiet PECL autorstwa Sary Golemon, który pozwoliłby ci przeciążać operatorów . Nie wiem, jak kompatybilny jest z PHP.current.
Gordon,
1
@Pooya To dlatego, że nodenie jest to własność $ foo, ale własność doesNotExist. Tak więc, chyba że „doesNotExist” jest obiektem (który implementuje __set lub ma publiczną właściwość o nazwie node), to nie zadziała.
Tivie,
1
@ Allen tak, to prawda.
Gordon,
34

Polecam użycie tablicy do przechowywania wszystkich wartości za pośrednictwem __set().

class foo {

    protected $values = array();

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

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

}

W ten sposób upewniasz się, że nie możesz uzyskać dostępu do zmiennych w inny sposób (uwaga, że $valuesjest chroniony), aby uniknąć kolizji.

Fidi
źródło
19

Z podręcznika PHP :

  • __set () jest uruchamiana podczas zapisywania danych do niedostępnych właściwości.
  • __get () służy do odczytywania danych z niedostępnych właściwości.

Jest to wywoływane tylko przy odczytywaniu / zapisywaniu niedostępnych właściwości. Twoja własność jest jednak publiczna, co oznacza, że ​​jest dostępna. Zmiana modyfikatora dostępu na chroniony rozwiązuje problem.

Berry Langerak
źródło
7

Aby rozwinąć odpowiedź Berry'ego, że ustawienie poziomu dostępu na protected pozwala __get i __set na użycie z jawnie zadeklarowanymi właściwościami (przynajmniej w przypadku dostępu poza klasą), a prędkość jest znacznie wolniejsza, zacytuję komentarz z innego pytania na ten temat i tak czy owak uzasadnij jego użycie:

Zgadzam się, że __get działa wolniej niż niestandardowa funkcja get (robi to samo), jest to 0,0124455 czas dla __get () i 0,0024445 dla niestandardowej funkcji get () po 10000 pętli. - Melsi Lis 23 '12 o 22:32 Najlepsza praktyka: Magiczne metody PHP __set i __get

Według testów Melsi, znacznie wolniejszy jest około 5 razy wolniejszy. Jest to zdecydowanie wolniejsze, ale należy również zauważyć, że testy pokazują, że nadal można uzyskać dostęp do właściwości za pomocą tej metody 10000 razy, licząc czas iteracji pętli, w przybliżeniu w 1/100 sekundy. Jest znacznie wolniejszy w porównaniu z rzeczywistymi zdefiniowanymi metodami pobierania i ustawiania, i to mało powiedziane, ale w ogólnym rozrachunku nawet 5 razy wolniej nigdy nie jest tak naprawdę powolne.

Czas obliczeniowy operacji jest nadal znikomy i nie warty rozważenia w 99% rzeczywistych aplikacji. Jedynym przypadkiem, którego naprawdę należy unikać, jest fakt, że zamierzasz uzyskać dostęp do właściwości ponad 10000 razy w jednym żądaniu. Witryny o dużym ruchu robią coś naprawdę złego, jeśli nie stać ich na wyrzucenie kilku serwerów więcej, aby ich aplikacje działały. Jednowierszowa reklama tekstowa w stopce witryny o dużym natężeniu ruchu, w której szybkość dostępu staje się problemem, mogłaby prawdopodobnie zapłacić za farmę 1000 serwerów z tym wierszem tekstu. Użytkownik końcowy nigdy nie będzie dotykał palcami, zastanawiając się, co tak długo trwa ładowanie strony, ponieważ dostęp do właściwości aplikacji zajmuje milionową część sekundy.

Mówię, że jako programista wywodzący się z doświadczenia w .NET, ale niewidoczne metody pobierania i ustawiania dla konsumenta nie są wymysłem .NET. Po prostu nie są one właściwościami bez nich, a te magiczne metody są dla programistów PHP zachowaniem, które pozwala nawet nazywanie ich wersji właściwości „właściwościami”. Ponadto rozszerzenie Visual Studio dla PHP obsługuje technologię Intellisense z chronionymi właściwościami, myślę, że mając na uwadze tę sztuczkę. Myślę, że przy wystarczającej liczbie programistów używających w ten sposób magicznych metod __get i __set programiści PHP dostroiliby czas wykonywania, aby zaspokoić potrzeby społeczności programistów.

Edycja: Teoretycznie chronione właściwości wydawały się działać w większości sytuacji. W praktyce okazuje się, że wiele razy będziesz chciał użyć swoich metod pobierających i ustawiających podczas uzyskiwania dostępu do właściwości w ramach definicji klasy i klas rozszerzonych. Lepszym rozwiązaniem jest klasa bazowa i interfejs do rozszerzania innych klas, więc możesz po prostu skopiować kilka wierszy kodu z klasy bazowej do klasy implementującej. Robię trochę więcej z klasą bazową mojego projektu, więc nie mam teraz interfejsu do udostępnienia, ale oto niesprawdzona, uproszczona definicja klasy z magiczną właściwością, która pobiera i ustawia za pomocą odbicia w celu usunięcia i przeniesienia właściwości do chroniona tablica:

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

Przepraszam, jeśli w kodzie są jakieś błędy.

Jason Ensinger
źródło
Znalazłem problem na 'access' => $ property-> getModifier ()
macki
5

To dlatego, że $ bar jest własnością publiczną.

$foo->bar = 'test';

Nie ma potrzeby wywoływania magicznej metody podczas uruchamiania powyższego.

Usunięcie public $bar;z klasy powinno to naprawić.

Matt Lowden
źródło
1

Najlepiej używać magicznych metod set / get z predefiniowanymi niestandardowymi metodami set / get, jak w przykładzie poniżej. W ten sposób możesz połączyć to, co najlepsze z dwóch światów. Jeśli chodzi o prędkość, zgadzam się, że są nieco wolniejsze, ale można nawet poczuć różnicę. Przykład poniżej również sprawdza poprawność tablicy danych względem predefiniowanych metod ustawiających.

„Metody magiczne nie zastępują metod pobierających i ustawiających. Pozwalają po prostu na obsługę wywołań metod lub dostępu do właściwości, które w przeciwnym razie spowodowałyby błąd”.

Dlatego powinniśmy używać obu.

PRZYKŁAD POZYCJI KLASY

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

WYNIK

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
źródło
0

Porzuć public $bar;deklarację i powinno działać zgodnie z oczekiwaniami.

Alix Axel
źródło
-6

Intenta con:

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

_SET($k,$v){
 return $this->$k = $v;
}
charly
źródło