Any time we do a backwards compatibility break we should stop to think about what other improvements we could make to the language while we are at it. Sometimes, drastic changes are needed. The jump from PHP 4 to PHP 5 was huge. It included a more modern object model and many other similar improvements. This RFC is an idea for when we jump from PHP 5 to PHP 6 and how we might improve upon the object model that we have. Since classes define new types, I will also discuss types in general.
This is not a comprehensive list.
Consider the following class that holds two properties, id and name. The class follows fairly conventional practices; its variables are not public and provides read / write access through get and set functions in order to future-proof code.
class Person { /** * @var int */ protected $id; /** * @var string */ protected $name; /** * @param int $id * @param string $name */ function __construct($id, $name) { $this->id = $id; $this->name = $name; } /** * @return int */ function getId() { return $this->id; } /** * @return string */ function getName() { return $this->name; } /** * @param int $id */ function setId($id) { $this->id = $id; } /** * @param string $id */ function setName($name) { $this->id = $name; } }
There is nothing in this code that prevents people from specifying incorrect types. If catching these errors is important enough, you have to introduce type checking with something like this:
class Person { // properties /** * @param int $id * @param string $name * @throws InvalidArgumentException */ function __construct($id, $name) { $this->setId($id); $this->setName($name); } // getters /** * @param int $id * @throws InvalidArgumentException */ function setId($id) { if (filter_var($id, FILTER_VALIDATE_INT) === FALSE) { throw new InvalidArgumentException(); } $this->id = $id; } /** * @param string $name * @throws InvalidArgumentException */ function setName($name) { if (is_bool($name) || !is_scalar($name)) { throw new InvalidArgumentException(); } $this->id = $name; } }
That's not even the end of it, though. What if our class really needs to have an interface? We end up with something like this:
interface Person { /** * @return int */ function getId(); /** * @return string */ function getName(); } class PersonImpl implements Person { protected $id; /** * @var string */ protected $name; /** * @param int $id * @param string $name */ function __construct($id, $name) { $this->id = $id; $this->name = $name; } /** * @return int */ function getId() { return $this->id; } /** * @return string */ function getName() { return $this->name; } /** * @param int $id * @throws InvalidArgumentException */ function setId($id) { if (filter_var($id, FILTER_VALIDATE_INT) === FALSE) { throw new InvalidArgumentException(); } $this->id = $id; } /** * @param string $name * @throws InvalidArgumentException */ function setName($name) { if (is_bool($name) || !is_scalar($name)) { throw new InvalidArgumentException(); } $this->id = $name; } }
Note that I've left off public visibility modifiers so there is less to look at, but it would not be abnormal to see them in the wild.
I feel like we can improve this by changing how types and more specifically objects work. By adding static types as an option (and supporting them with type-hints), creating automatic getters and setters, and reducing the need for some keywords, we can cut out most of that code. An example is worth a thousand words:
interface Person { int get id(); string get name(); } class PersonImpl implements Person { int $id; // creates get and set methods for id string $name; // creates get and set methods for name __construct(int $id, string $name) { $this->id = $id; // calls the set id method $this->name = $name; // calls the set name method } }
The class definition would be exactly equivalent to this PHP 6 syntax:
class PersonImpl implements Person { private int $__id; private string $__name; public __construct(int $id, string $name) { $this->id = $id; // calls the set id method $this->name = $name; // calls the set name method } public int get id() { return $this->__id; } public string get name() { return $this->__name; } public void set id(int $id) { $this->__id = $id; } public void set name(string $name) { $this->__name = $name; } }
Notes:
Q: Why do we want the syntax `$object->property;` to trigger a get method and `$object->property = 'prop';` to trigger a set method?
A:
class Object { var $property; } $object = new Object(); $object->property = 'name'; // Later on I need to upper-case the value when it is set, so I modify the class: class Object { private var $property; function getProperty() { return $this->property; } function setProperty($property) { $this->property = touppercase($property;) } } // code like the following will now break: $object->property = 'name';
You could use `_get` and `_set` magic but they are really a poor-man's get and set implementation. They are also very hard to build tools for.
$object = new Object(); $object->property = 'foo'; //obviously accessing a property $object->setProperty('foo'); // argue about clarity if you want, but it's less obvious to me