Na navigaci | Klávesové zkratky

Property setters and getters - final solution

PHP currently supports property overloading provided by magic methods __get() and __set(). Those methods are called when accessed property is not declared in the class. Actually this overloading is a bit frustrating.

This is my solution how to simulate properties with accessor methods in a Delphi way.

So…

// define new "like-keyword"
// value is short and unusual string
define('property',   "\0\0");

Maybe _property is better choice. It doesn't matter now.

And the Initializator:

function _property($obj)
{
  // use cache (analyze object only once)
  static $cache;
  $props = & $cache[ get_class($obj) ];

  if ($props === NULL) {
    // build list of properties
    // 1) get_object_vars prevents ObjectIteration
    // 2) outside of objects returns only public members
    $props = array_flip(array_keys(get_object_vars($obj), property, TRUE));
  }

  foreach ($props as $name => $foo) {
    unset($obj->$name); // unset property to pass control to __set() and __get()
  }

  return $props;
}

See documented source code of class:

class Rectangle
{
  // list of properties
  private $_props;

  // shadow properties
  private $_width;
  private $_height;


  /**
   * @var int  phpDoc for virtual property!
   * getter: $_width  setter: setWidth
   */
  public $width  = property;
  public $height = property;
  public $area   = property;


  function __construct()
  {
    // property initialization
    $this->_props = _property($this);
  }



  /**
   * Universal setter.
   *
   * If property 'abc' is declared,
   * there must be setAbc() setter,
   * otherwise property is read-only
   */
  final function __set($name, $value)
  {
    if (!isset($this->_props[$name])) {
      throw new Exception("Undefined property '$name'.");
    } elseif (method_exists($this, 'set'.$name)) {
      $this->{'set'.$name}($value);
    } else {
      throw new Exception("Property '$name' is read-only.");
    }
  }


  /**
   * Universal getter.
   *
   * If property 'abc' is declared,
   * there must be getAbc() getter,
   * otherwise variable $_abc
   */
  final function &__get($name)
  {
    if (!isset($this->_props[$name])) {
      throw new Exception("Undefined property '$name'.");
    } elseif (method_exists($this, 'get'.$name)) {
      return $this->{'get'.$name}();
    } else {
      return $this->{'_'.$name};
    }
  }


  // getters and setters:

  protected function setWidth($value)
  {
    $this->_width = abs($value);
  }

  protected function setHeight($value)
  {
    $this->_height = abs($value);
  }

  protected function getArea()
  {
    return $this->_height * $this->_width;
  }

}


// usage:

$rect = new Rectangle();
$rect->width = 10;
$rect->height = 5;

echo $rect->area;
// prints 50

// try receive private member
echo $rect->props;
// throw exception

Pros:

  • universal __set & __get, no overriden is needed
  • magic methods know if specific property exists
  • so this mechanism cannot be misused to get private members
  • properties are case-sensitive, like other variables
  • these __set & __get works very fast (no switches or ifs)
  • virtual properties may be documented with any of the existing tools (phpDoc, doxygen)
  • IDE autocomplete based on phpDoc will work
  • easy adaptive for future PHP6 (I hope)

Cons:

  • overloading is still not safe (see bug #36484, fixed in PHP 5.2.1)

Since PHP 5.1.0 it is also possible to overload the isset() and unset():

final private function __isset($name)
{
    return isset($this->_props[$name]);
}

final private function __unset($name)
{
    unset($this->_props[$name]);
}

Some links:

Komentáře

  1. Milf #1

    Já výše uvedenému nerozumím, tak se zeptám – k čemu je uvedené dobré?

    • programátor je tvor líný
    • je třeba exceptions při nevhodném přístupu
    • něco jiného, mně skrytého

    Předem děkuji za vysvětlení. Z teorie mám něco vyčteno a praxe zdá se mi býti poněkud odlišná, ztrácím se v problematice.

    p.s. snad nevadí, že se nedržím angličtiny, bylo by to asi veselejší

    před 11 lety | reagoval [3] David Grudl
  2. Milf #2

    Ok, question in English comes now :-P
    Manual says about setters/getters …

    These methods will only be triggered when your object or inherited object doesn't contain the member or method you're trying to access.

    My question extends previous question (Czech one) – when and why it happens, what is wrong if members don't exist and someone is trying to access them?
    I have no idea, I am thinking about not so good analysis.

    před 11 lety
  3. David Grudl http://davidgrudl.com #3

    avatar
    před 11 lety | reagoval [4] Milf
  4. Milf #4

    #3 David Grudl, ok, díky za materiál k samostudiu ;-) Z toho co jsem zatím přečetl (6 stran) se mi nejvíce zamlouvá tato myšlenka:

    Our current poll results really reflect Fowler's quote: there seem to be two equally populated camps/schools regarding how to use accessor/modifier methods.

    :-P
    před 11 lety
  5. llook http://llook.wz.cz/weblog/ #5

    avatar

    I have totaly refused to use __get and __set magics. You have to take more care, than with POJO-like getters and setters. I think that $this->foo++ requires more imagination than $this->setFoo($this->getFoo() + 1).

    For example, $this->foo++ modifies the property, but $this->foo[ 1] = 'bar' doesn't (as seen in your previous article). It could sometimes be a bit confusing.

    So only one overloading function that I am currently using is __call, the code is similar to this:

    class Foo {
        private $fields = array();
        public function __call($method, $args) {
            $getset = substr($method, 0, 3);
            $property = substr($method, 3);
            if ($getset === 'get') {
                return isset($this->fields[$property])
                        ? $this->fields[$property]
                        : null;
            } elseif ($getset === 'set') {
                $value = isset($args[0]) ? $args[0] : null;
                $this->fields[$property] = $value;
            } // else...
        }
    }

    I can use Java-like setters and getters without the amount of methods with the same code (which is so typical for Java-like [gs]etters in Java) and I am always sure if I am calling the setter.

    And when I realy need to encapsulate a field, I just write proper get/set methods (a setter throwing exception for readonly fields) with no need of API modification.

    Before time I was also using __get/__set in this way:

    public function __get($name) {
        $method = 'get' . ucfirst($name);
        return $this->$method();
    }
    public function __set($name, $value) {
        $method = 'set' . ucfirst($name);
        $this->$method($value);
    }

    But then I had found that I am only using get? and set?, so I have dropped those two methods out.

    Co je to za novou módu tyhlety dvojjazyčný weblogy?

    před 11 lety | reagoval [8] David Grudl [9] David Grudl
  6. llook http://llook.wz.cz/weblog/ #6

    avatar

    Mám návrh na vylepšení – odkazy na ostatní komentáře nějak odlišit už v náhledu.

    před 11 lety
  7. hvge http://hvge.sk #7

    Offtopic: Tie vase priklady v PHP 5 ma motivovali pridat chybajuce klucove slova do FSHL :)

    před 11 lety
  8. David Grudl http://davidgrudl.com #8

    avatar

    #5 llook, Komentovat můžeš klidně česky. Já jsem k tomuto zveřejněnému postupu otevřel diskuzi na SitePoint a proto ta angličtina.

    Jinak přesně ty “arrays via getters” jsou to hlavní, co mě stále od jejich použití odrazuje. Tohle musím ještě domyslet…

    před 11 lety
  9. David Grudl http://davidgrudl.com #9

    avatar

    #5 llook, how encapsulate an array:

    ...
    final function __get($name)
    {
        // $this->_props[$name] is array
        return new ArrayObject($this->_props[$name]);
    }
    ...
    
    $this->foo[4619] = 'bar';

    or simply with ampersand:

    ...
    final function &__get($name)
    {
        return $this->_props[$name];
    }
    ...
    
    $this->foo[4619] = 'bar';
    před 11 lety | reagoval [11] Štěpán Svoboda
  10. GgzeE #10

    Už to asi není aktuální, ale pošlu sem mojí úpravu kódu v článku pro případné zájemce.

    Vysvětlení příkladu:

    • Kód píšu zásadně česky, proto jsem si přeložil veškeré názvy proměnných z příkladu do češtiny.
    • Zásadní změnou je uzavření obslužných metod do abstraktní (virtuální, aby z ní nemohly být vytvořeny instance) třídy Vlastnosti, tu stačí pouze zdědit a můžete začít používat postupy, který je vidět v testovací třídě.
    • V mém kódu jsem také vytvořil 3 konstanty pro vlastnosti pouze pro čtení/zápis a obojí.
    • Stejně jako u metody __get je možné vracet pouze hodnotu _proměnné, tak i u __set, pokud neexistuje metoda nastavProměnná, je možné přímo nastavit _proměnnou (ovšem, kvůli dědičnosti musí být protected a ne private – to nyní platí i u __get)
    • Metodu pro vyhledávání jsem uzavřel jako statickou do třídy Vlastnosti a vyhledávání automaticky probíhá v konstruktoru (v případě psaní vlastního konstruktoru dceřinné třídě je potřeba zavolat konstruktor rodiče)

    Takže první obsah souboru vlastnosti.php (třída Vlastnosti):

    define("VLASTNOSTPROCTENI", 1);
    define("VLASTNOSTPROZAPIS", 2);
    define("VLASTNOST", VLASTNOSTPROCTENI | VLASTNOSTPROZAPIS);
    
    abstract class Vlastnosti {
      private $_vlastnosti;
    
      function __construct() {
        $this->_vlastnosti = self::_vyhledejAUlozVlastnosti($this);
      }
    
      private static function _vyhledejAUlozVlastnosti($objekt) {
        static $cacheVlastnosti;
        $vlastnosti = &$cacheVlastnosti[get_class($objekt)];
    
        if($vlastnosti === NULL) {
          foreach(get_object_vars($objekt) as $nazev => $hodnota) {
            if(($hodnota & VLASTNOSTPROZAPIS) == VLASTNOSTPROZAPIS or ($hodnota & VLASTNOSTPROCTENI) == VLASTNOSTPROCTENI) {
              $vlastnosti[$nazev] = $hodnota;
              unset($objekt->$nazev);
            }
          }
    
          return $vlastnosti;
        }
    
        foreach($vlastnosti as $nazev => $hodnota)
          unset($objekt->$nazev);
    
        return $vlastnosti;
      }
    
      final function __set($nazev, $hodnota) {
        if(isset($this->_vlastnosti[$nazev])) {
          if($this->_vlastnosti[$nazev] !== VLASTNOSTPROCTENI) {
            if(method_exists($this, "nastav$nazev"))
              $this->{"nastav$nazev"}($hodnota);
            else
              $this->{"_$nazev"} = $hodnota;
          } else
            throw new Exception("Vlastnost $nazev třídy ".get_class($this)." je pouze pro čtení!");
        } else
          throw new Exception("Vlastnost $nazev třídy ".get_class($this)." není definována!");
      }
    
      final function __get($nazev) {
        // $this->_props[$name] is array
        //return new ArrayObject($this->_props[$name]);
    
        if(isset($this->_vlastnosti[$nazev])) {
          if($this->_vlastnosti[$nazev] !== VLASTNOSTPROZAPIS) {
            if(method_exists($this, "vrat$nazev"))
              return $this->{"vrat$nazev"}();
            else
              return $this->{"_$nazev"};
          } else
            throw new Exception("Vlastnost $nazev třídy ".get_class($this)." je pouze pro zápis!");
        } else
          throw new Exception("Vlastnost $nazev třídy ".get_class($this)." není definována!");
      }
    }

    Obsah souboru testovaciTrida.php (ukázka testovací třídy za využití třídy Vlastnosti):

    class TestovaciTrida extends Vlastnosti {
      private $_sirka;
      private $_vyska;
      protected $_test;
      protected $_test2 = 10;
      // Definice vlastností
      public $sirka   = VLASTNOST;
      public $vyska   = VLASTNOSTPROZAPIS;
      public $obsah   = VLASTNOSTPROCTENI;
      public $test    = VLASTNOST;
      public $test2    = VLASTNOSTPROCTENI;
    
      protected function nastavSirka($hodnota) {
        $this->_sirka = abs($hodnota);
      }
    
      protected function nastavVyska($hodnota) {
        $this->_vyska = abs($hodnota);
      }
    
      protected function vratObsah() {
        return $this->_vyska * $this->_sirka;
      }
    }

    A konečně obsah index.php (testování testovací třídy :-)):

    require_once("vlastnosti.php");
    require_once("testovaciTrida.php");
    
    $test = new TestovaciTrida();
    
    $test->sirka = 10;
    $test->vyska = 5;
    
    $test->test = array();
    $test->test[] = 5;
    echo $test->test[0];
    $test->test[] = 10;
    echo $test->test[4619];
    $test->test[4619]++;
    echo $test->test[4619];
    
    echo $test->test2;
    
    echo $test->obsah;
    
    // Vyvolání vyjímek ->
    //$test->area = 5;
    //echo $test->height;

    Tak snad Vám můj kód bude k užitku…

    před 10 lety
  11. Štěpán Svoboda http://develo.styled.cz #11

    avatar

    #9 David Grudl, V podstatě tam ty public properties jsou jenom proto aby šli dokumentovat phpdocem a fungovalo ide autocomplete. Nebo mají ještě nějaký jiný význam?

    před 10 lety | reagoval [12] Štěpán Svoboda
  12. Štěpán Svoboda http://develo.styled.cz #12

    avatar

    #11 Štěpán Svoboda, teda ne že by tyhle dvě věci nestačili ;-)

    před 10 lety
  13. Štěpán Svoboda http://develo.styled.cz #13

    avatar

    Ještě tady nikdo nezmínil možnost, že by Property byl objekt. Už dlouho o tom přemýšlím a nezdá se mi to tak úplně ztracené. Taková Vlastnost by potom mohla mít třeba masku nebo rozsah hodnot…

    Ano, dá se to řešit setterem, ale to se mi právě moc nelíbí. Objektově zpracované události by mohli těžit z možností jež nám nabízí dědění…

    class Property
    {
        private $value;
        private $type;
        private static $types = array('string', 'date', 'time', 'datetime', 'integer', 'float', 'boolean', 'enum', 'set');
        private $validation = null;
        public $autotrim = true;
        public $unique = false;
        private $members = array();
    
        public function setValue($value)
        {
            // Assign value in specified type
            switch ($this->type) {
                case 'string': {
                    if ($this->autotrim) {
                        $this->value = trim($value);
                    }
                    break;
                }
                case 'date': {
                    $this->value = $value;
                    break;
                }
            }
            $this->value = $value;
        }
    
        public function getValue()
        {
            return $this->value;
        }
    
    }
    před 10 lety
  14. Štěpán Svoboda http://develo.styled.cz #14

    avatar
    class PropertyEmail extends Property
    {
    function __construct()
    {
    $this->mask = "^[_a-zA-Z0-9\.\-]+@[_a-zA-Z0-9\.\-]+\.[a-zA-Z]{2,4}$";
    }
    }

    offtopic: jestli tenhle komentář už bude barevnej tak jsem se na nic zeptat nechtěl ;) a jestli ne tak jsem se chtěl zeptat jak udělam barevnej php kód

    před 10 lety
  15. Wouter http://n/a #15

    avatar

    The use of getters and setters (in C#) is to CONTROL the way properties (class variables) are used by instances. NOT to look good! So the getter is simply to return the value of the property and the setters is to control the setting of the value of the property. If for some reason you use this to mimic the C# getters & setters, you loose all benefit: CONTROL OVER VALUE OF PROPERTIES. I don't understand why PHP would call this getters and setters, it's just not the same.

    před 9 lety

Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.