Klávesové zkratky na tomto webu - rozšířené Na obsah stránky

Translate to English… Ins Deutsche übersetzen…

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:

napsáno 2. 3. 2006 | shlédnuto 9941x | nahoru


Komentáře RSS 2.0 komentářů » přidat

#1 Milf nový

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ší

Posláno 2. 3. 2006 v 7.47 | Odpovědět

#2 Milf nový

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.

Posláno 2. 3. 2006 v 8.11 | Odpovědět
avatar

#3 David Grudl http://davidgrudl.com nový

Posláno 2. 3. 2006 v 8.17 | Odpovědět
Na komentář reagoval [4] Milf

#4 Milf nový

#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
Posláno 2. 3. 2006 v 8.55 | Odpovědět
avatar

#5 llook http://llook.wz.cz/weblog/ nový

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?

Posláno 2. 3. 2006 ve 12.45 | Odpovědět
Na komentář reagoval [8] David Grudl [9] David Grudl
avatar

#6 llook http://llook.wz.cz/weblog/ nový

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

Posláno 2. 3. 2006 ve 12.47 | Odpovědět

#7 hvge http://hvge.sk nový

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

Posláno 2. 3. 2006 v 15.17 | Odpovědět
avatar

#8 David Grudl http://davidgrudl.com nový

#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…

Posláno 2. 3. 2006 v 18.19 | Odpovědět
avatar

#9 David Grudl http://davidgrudl.com nový

#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';
Posláno 8. 5. 2006 v 18.23 | Odpovědět
Na komentář reagoval [11] Štěpán Svoboda

#10 GgzeE nový

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…

Posláno 14. 6. 2006 ve 14.29 | Odpovědět
avatar

#11 Štěpán Svoboda http://develo.styled.cz nový

#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?

Posláno 3. 1. 2007 v 19.33 | Odpovědět
Na komentář reagoval [12] Štěpán Svoboda
avatar

#12 Štěpán Svoboda http://develo.styled.cz nový

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

Posláno 3. 1. 2007 v 19.35 | Odpovědět
avatar

#13 Štěpán Svoboda http://develo.styled.cz nový

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

}
Posláno 5. 1. 2007 v 16.08 | Odpovědět
avatar

#14 Štěpán Svoboda http://develo.styled.cz nový

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

Posláno 5. 1. 2007 v 16.14 | Odpovědět
avatar

#15 Wouter http://n/a nový

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.

Posláno 9. 7. 2007 v 16.06 | Odpovědět

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

Výtah na začátek článku na první komentář

Názory čtenářů v diskusích nejsou názory provozovatele webu, a ten za jejich obsah neodpovídá.

phpFashion © 2004, 2012 David Grudlo webu

Pokud není uvedeno jinak, podléhá obsah těchto stránek licenci Creative Commons BY-NC-ND Creative Commons License BY-NC-ND

Ukázky zdrojových kódů smíte používat s uvedením autora a URL tohoto webu bez dalších omezení.