...

View Full Version : Enforcing class property types



hessodreamy
08-17-2012, 02:52 PM
I've got a financial system where I'd like to make many financial properties into instances of a Charge class, which encapsulate tax information etc, rather than constantly defining vat/ex vat variables & properties.

What I'd like to do is allow such properties to be set as normal numbers, partly for ease of coding, and partly for compatibility with legacy code. What would be the best way to do this?

I could use magic functions, I guess:



class Charge
{
public $net;
public $gross;
public $vat;
public $vatRate = 0.2;

public function __construct($net)
{
$this->net = $net;
$this->vat = round($this->net*$this->vatRate,2);
$this->gross = $this->net + $this->vat;
}
}

class Myclass
{
private $price;

public function __set($name,$val)
{
if($name=="price") $this->setPrice($val);
}

public function setPrice($price)
{
//enforce price into charge class
if(! $price instanceof Charge) $price = new Charge($price);
$this->price = $price;
}
}

$class = new MyClass();
$class->price=3;

but I've heard magic functions are pretty slow, also, to get the magic function to work I'd either need to not declare my variable initially (not keen on that), or declare it as private, in which case the magic function would work when reference from outside the class, but not when referenced from within the class or it's subclasses.

Or I could just declare it as private and make sure the setter is used explicitly at all times.

What would be a good approach to this?

**EDIT**
I just came up with another idea - declare all class properties prepended with an underscore eg private $_price;
You can reference properties in the normal way ($class->price) and, as the property will never exist, will always refer to the magic functions. The magic function then knows to access the property usng the underscore eg

private $_price;
public function __set($name,$val)
{
if($name=="price") $this->_price = new Charge($val);//for example
}

This at least gets around the issue of using a consistent reference when referring from within this class or subclasses. Still got a possible speed issue, though.

Fou-Lu
08-17-2012, 06:03 PM
Never never never ever use public properties in PHP. Ever. The only property in PHP that should be public is the use of const instead. It is datatype weak, you don't have an option but to enforce your datatypes at a set level.

I wouldn't use magic functions either. I'd write accessors and mutators instead. Since arrays and object types can be typehinted (and if you write proper error handlers, so can primitives, but IMO that's a complete waste of processing), then it makes more sense to use a setter than a __set. __set will also become increasingly complicated to debug as more properties and rules are added.

This is what I'd do:


public function setPrice($price)
{
//enforce price into charge class
if(! $price instanceof Charge) $price = new Charge($price);
$this->price = $price;
}


Although, if you allow one numerical and one object type entry, I'd probably reflect it as such:


public function setPrice($dPrice)
{
if (!is_numeric($dPrice))
{
throw new InvalidArgumentException("Price must be a double!");
}
$this->setPriceCharge(new Charge($dPrice));
}

public function setPriceCharge(Charge $c)
{
$this->price = $c;
}


And with __set, I do this in my base object class:


public function __set($var, $val)
{
throw new RuntimeException("Cannot set to property " . $var);
}


Automatic property creation in PHP is one of the stupidest thing's I've ever seen.

If you *really* want property set ability, chain it from a set into a setter instead. Its much cleaner to look at. Assuming that you prepend all property names to a setter prefixed with set, then you can use reflection as well to determine the method to call.

hessodreamy
08-20-2012, 01:00 PM
Thanks for the response. So what you're saying is: Don't be so lazy. write some proper getters & setters? Good plan.


If you *really* want property set ability, chain it from a set into a setter instead. Its much cleaner to look at. Assuming that you prepend all property names to a setter prefixed with set, then you can use reflection as well to determine the method to call.

Can you explain what you mean by this?

Fou-Lu
08-20-2012, 06:17 PM
The easiest way is by use of a standard naming convention and reflection.


public function __set($member, $value)
{
try
{
$sFunc = 'set' . $member;
$rf = new ReflectionMethod($this, $sFunc);
$rf->invoke($this, $value);
}
catch (ReflectionException $ex)
{
throw $ex;
}
}


And if you used it as such:


<?php

class Num
{
private $num;

public function getNum()
{
return $this->num;
}

public function setNum($num)
{
if (!is_numeric($num))
{
throw new InvalidArgumentException('Provided argument is not a number');
}
$this->num = $num;
}

public function __set($member, $value)
{
try
{
$sFunc = 'set' . $member;
$rf = new ReflectionMethod($this, $sFunc);
$rf->invoke($this, $value);
}
catch (ReflectionException $ex)
{
throw $ex;
}
}
}

$n = new Num();
print_r($n);
$n->setNum(5);
print_r($n);
$n->num = 10;
print_r($n);
$n->num = 'cat';
print_r($n);



You'd have a result of:


Num Object
(
[num:private] =>
)
Num Object
(
[num:private] => 5
)
Num Object
(
[num:private] => 10
)

Fatal error: Uncaught exception 'InvalidArgumentException' with message 'Provided argument is not a number' in /t.php:16
Stack trace:
#0 [internal function]: Num->setNum('cat')
#1 /t.php(27): ReflectionMethod->invoke(Object(Num), 'cat')
#2 /t.php(42): Num->__set('num', 'cat')
#3 {main}
thrown in /t.php on line 16



EZ Archive Ads Plugin for vBulletin Copyright 2006 Computer Help Forum