Wednesday 17 September 2014

Looking at PHP's OOP from a CFMLer's perspective: overloading (which is not what you'd assume it is)

G'day:
This is the fifth part of a series. The first four parts are here:
Today I'm looking quickly at a PHP concept called "overloading". Which has nothing to do with the usual OO concept of "overloading", either in the sense of same-named methods with different signatures, or operator overloading. In fact I seriously dunno why they called this construct overloading at all.

In PHP parlance "Overloading" is basically a mix of what in CFML we'd refer to as synthesised and implicit accessors.

First up, here's a CFML recap.

Synthesised accessors

Synthesised accessors (often erroneously referred to as implicit accessors, which is a different thing entirely: see further down). This is a feature of CFML wherein one can explicitly call getter or setter methods on an object without the relevant accessor being defined. EG:

// Number.cfc
component accessors=true {

    property english;
    property maori;

    public function onMissingMethod(){
        writeDump(var=arguments, label="#getFunctionCalledName()#() call caught by onMissingMethod()");
    }

}

// testNumber.cfm

number = new Number();
number.setEnglish("toru");
number.setMaori("three");

number.setJapanese("三");


writeDump(var={
    english    = number.getEnglish(),
    maori    = number.getMaori()
}, label="From getters");

Here I have a simple CFC with two properties specified: english and maori, and the only method is onMissingMethod(), which is only used to demonstrate what happens when we call an accessor on a non-existent property.

The test code calls setEnglish() and setMaori(), which are synthesised setters for the english and maori properties. It also calls setJapanese() which is not a synthesised setter, as there is no such properly as japanese in this example.

The results are as follows:

onmissingmethod() call caught by onMissingMethod()
Scope Arguments
11
stringsetJapanese
22
Scope Arguments
11
string
From getters
Struct
ENGLISH
stringtoru
MAORI
stringthree

Note that setEnglish() and setMaori() do not trigger onMissingMethod() despite not actually being defined in the component; yet they still work fine, and set the properties correctly. On the other hand setJapanese() doesn't exist, and is simply caught by onMissingMethod().

Implicit Accessors

CFML also has implicit accessors (this needs to be switched on in Application.cfc). Implicit accessors are invoked implicitly (ie: without explicitly using them) when a property of a CFC is accessed. Here's an example:

// Application.cfc
component {
    this.invokeImplicitAccessor = true;
}

// Person.cfc
component {

    property firstName;
    property lastName;

    public string function getFirstName(){
        writeOutput("#getFunctionCalledName()#() called<br>");
        return firstName;
    }

    public void function setFirstName(firstName){
        writeOutput("#getFunctionCalledName()#() called with #firstName#<br>");
        variables.firstname = arguments.firstName;
    }

    public string function getLastName(){
        writeOutput("#getFunctionCalledName()#() called<br>");
        return lastName;
    }

    public void function setLastName(lastName){
        writeOutput("#getFunctionCalledName()#() called with #lastName#<br>");
        variables.lastName = arguments.lastName;
    }

}

// testPerson.cfm

person = new Person();
person.firstName = "Donald";
person.middleName = "Adam";
person.lastName = "Cameron";


writeOutput("Full name: #person.firstName# #person.lastName#<br>");

writeDump(person);

This time the accessor methods are defined, but they are never called. Simply referencing the property implicitly calls either the setter or the getter for it, as appropriate. Output:

setfirstname() called with Donald
setlastname() called with Cameron
getfirstname() called
getlastname() called
Full name: Donald Cameron

Component (Person) 
Only the functions and data members that are accessible from your location are displayed
public
MIDDLENAME
stringAdam
GETFIRSTNAMEvar.GETFIRSTNAME
GETLASTNAMEvar.GETLASTNAME
SETFIRSTNAMEvar.SETFIRSTNAME
SETLASTNAMEvar.SETLASTNAME

The thing to note here is that the setters and getters for firstName and lastName are indeed being invoked, despite no explicit reference to them. Also note that setting middleName does not result in any special action, it just sticks a variable into the this scope of the object.

Combining synthesised and implicit accessors

And the two concepts can be combined. Here's an example where the accessors are synthesised, and they are called implicitly:

// Day.cfc
component accessors=true {

    property english;
    property maori;

}

// testDay.cfm

day = new Day();
day.english    = "Tuesday";
day.maori    = "Rātū";
day.japanese= "火曜日";

writeDump(day);

I'll not pore over this, but it just demonstrates there's no accessor methods defined, plus we never call them. But - as if by magic - the behaviour is as if they were there and they were called:

Component (Day) 
Only the functions and data members that are accessible from your location are displayed
Properties
english
stringTuesday
maori
stringRātū
public
getMaorivar.getMaori
setEnglishvar.setEnglish
getEnglishvar.getEnglish
JAPANESE
string火曜日
setMaorivar.setMaori

Note that Railo actually does create the methods for me, behind the scenes. Note also that the japanese value is simply treated as an assignment of a this-scoped variable in the day object.

So that's the CFML scene set. How does PHP deal with this?

Overloading

I have no idea why PHP decided to call this overloading. Even PHP itself realises the confusion it would cause by co-opting an established term and using it to mean something different. This is from the docs:
Note:
PHP's interpretation of "overloading" is different than most object oriented languages. Overloading traditionally provides the ability to have multiple methods with the same name but different quantities and types of arguments.
So they knew they were doing something stupid, and still did it anyhow. This is reminiscent of some decisons Adobe has made with CFML. Except generally Adobe seem to be completely oblivious to poor decisions they make, I dunno which is worse. But I digress.

<?php
// Person.class.php

class Person
{
    private $firstName;
    private $lastName;
    private $sex;

    public function __set($name, $value)
    {
        echo sprintf("%s() called with %s, %s<br>", __FUNCTION__, $name, $value);
        $this->$name = $value;
    }

    public function __get($name)
    {
        echo sprintf("%s() called with %s<br>", __FUNCTION__, $name);
        return $this->$name;
    }

    public function __isset($name)
    {
        echo sprintf("%s() called with %s<br>", __FUNCTION__, $name);
        return isset($this->$name);
    }

    public function __unset($name)
    {
        echo sprintf("%s() called with %s<br>", __FUNCTION__, $name);
        unset($this->$name);
    }

    public function setSex($sex)
    {
        $this->sex = $sex;
    }

    public function getSex()
    {
        $this->sex = $sex;
    }

}

So PHP goes wild with its underscores again here. Neat :-/

Overloading provides a number of methods __set(), __get(), __isset(), __unset() which are implicitly invoked when a property is accessed. One cool thing here is that as well as the obvious getter/setter treatment, it also caters for when testing if the property is set, and also when the property is "unset" (see example below). It's ugly as hell, but it all works OK.

We've also got another even more egregious underscore fiesta with the __FUNCTION__ "magic constant". It's kinda (kinda) like getFunctionCalledName() in CFML. I'll do another quick article about this shortly.

One quick thing to note in this code is this:

$this->$name = $value;

Note that this is different from a normal literal property assignment, eg:
$this->firstName = $firstName;

In the former example, the name of the key being set is itself a variable, not a literal as per the latter example. So in the __set() (etc) methods, the $name variable is the name of the property. Not a property called "name", if that makes sense. Same as doing this[name] in CFML.

Here's the test harness:

<?php
// test.php

require "Person.class.php";

$person = new Person();
$person->firstName = "Donald";
$firstName = $person->firstName;
echo "First name: $firstName<br>";
echo "<hr>";

if (!isset($person->lastName)){
    $person->lastName = "Cameron";
}
$lastName = $person->lastName;
echo "Last name: $lastName<br>";
unset($person->lastName);
echo "<hr>";

$person->middleName = "Adam";
$middleName = $person->middleName;
echo "Middle name: $middleName<br>";
echo "<hr>";

$person->sex = "Male";
$sex = $person->sex;
echo "Sex: $sex<br>";


This is simple (and much the same as the CFML example above). Here I test setters, getters, isset() and unset(). I also set/get a property which is not previously defined, for good measure. Result:

__set() called with firstName, Donald
__get() called with firstName
First name: Donald



__isset() called with lastName
__set() called with lastName, Cameron
__get() called with lastName
Last name: Cameron
__unset() called with lastName


__set() called with middleName, Adam
Middle name: Adam



__set() called with sex, Male
__get() called with sex
Sex: Male


Observations:
  • The handling of $firstName and $lastName is predictable.
  • There seems to be a slight glitch when using overloading with undeclared properties: the __set() is called, but the __get() isn't? However the value is still fished out correctly. Weird. I have some googling to do there.
  • If a property has explicit accessors defined, they are not called. The docs made no claim to, but it'd be nice if they were. The implementation seems incomplete to me here.
Results of googling:
I found the answer to the issue with __get() not being called on access to $middleName. It had come up on Stack Overflow: "__get is not called if __set is not called, however code works?". It's not exactly the same, but this explains it:
In your second example, you assigned a value to a property in your object. Since you did not declare the property in your object, it gets created by default as public. Since its public, __get is never called when accessing it.
So this is actually working the same as the middleName & japanese examples from the CFML demos. Cool. Thanks, Stack Overflow.

I probably waffled on more about CFML in this than PHP, but it took me a while to refresh my memory on how CFML worked to contrast it with PHP, and to check my expectations.

OK, next I'm gonna have a look at this __FUNCTION__ thing...

--
Adam