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:

19 years ago in section PHP | blog written by David Grudl | back to top

You might be interested in

Comments

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

  2. Milf #2

    Ok, question in English comes now 😛
    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.

    19 years ago
  3. David Grudl #3

    avatar
    19 years ago | 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.

    😛

    19 years ago
  5. llook #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[smazáno]) ? $args[smazáno] : 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?

    19 years ago | reagoval [8] David Grudl [9] David Grudl
  6. llook #6

    avatar

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

    19 years ago
  7. hvge #7

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

    19 years ago
  8. David Grudl #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…

    19 years ago
  9. David Grudl #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#1 Milf = 'bar';

    or simply with ampersand:

    ...
    final function &__get($name)
    {
        return $this->_props[$name];
    }
    ...
    
    $this->foo#1 Milf = 'bar';
    19 years ago | 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#1 Milf;
    $test->test#1 Milf++;
    echo $test->test#1 Milf;
    
    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…

    19 years ago
  11. Štěpán Svoboda #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?

    18 years ago | reagoval [12] Štěpán Svoboda
  12. Štěpán Svoboda #12

    avatar

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

    18 years ago
  13. Štěpán Svoboda #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;
    	}
    
    }
    18 years ago
  14. Štěpán Svoboda #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

    18 years ago
  15. Wouter #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.

    17 years ago

This article has been closed. It is no longer possible to add comments.


phpFashion © 2004, 2024 David Grudl | o blogu

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