Wednesday, 10 September 2014

Looking at PHP's OOP from a CFMLer's perspective: traits

G'day:
As the title says, this is the third part of a series. The first two parts are here:
I suspect this will be the last part on this topic, but don't hold me to that just yet. Let's see how it goes (update: no, not by a long march... I'm writing part 4 now, and there'll be more after that).

Basically I'm working my way down the topics on the "Classes and Objects" page of the PHP reference, and getting myself up to speed with how PHP does it's OO. And documenting my observations as I go.

Traits

Traits are interesting. They're analgous to modules in Ruby (which I touch on in in equivalent newbie fashion to this article in "Ruby: doing a second tutorial @ codeschool.com (cont'ed)"). Basically they're a way to kinda emulate multiple inheritance in PHP, by using mixins. One thing that struck me as I was looking at the PHP approach is how bloody leaden the Ruby approach is. This is from a superficial assessment and usage requirements anyhow.

Basically a trait is a file which holds a bunch of functions, properties, what-have-you which can be used by a class to fill-out its methods. They're kinda like implemented interfaces in a way, I suppose.

In CFML one might achieve similar ends by include-ing a file with some UDFs in it, but traits are more fully-realised than taking that approach in CFML.

The docs explain everything fairly thoroughly, so I'll just show my experimentation code here.

Firstly I have changed my app_autoload.php (see "Autoloading classes" for the initial implementations of this) to also accommodate trait files:

<?php
// app_autoload.php
spl_autoload_register(null, false);
spl_autoload_extensions('.class.php, .interface.php, .trait.php');
spl_autoload_register(function($class){
    $classesDir = dirname(__FILE__) . "/classes";
    forEach (["class", "interface", "trait"] as $fileType) {
        $filePath = "$classesDir/$class.$fileType.php";
        if (is_file($filePath)){
            return require $filePath;
        }
    }
});

So I'm just looping over an array of file types now, instead of hard-coding each type.

There's a coupla new (to me) constructs in there:
  • dirname() is the PHP equivalent of getDirectoryFromPath().
  • __FILE__ (yes, two underscores either side. Nice one, PHP) is the equivalent to CFML's getCurentTemplatePath().
The inference here is that I've now got three sorts of files in my "classes" directory: Foo.class.php, Foo.interface.php and Foo.trait.php. Not interesting.

For this exercise, I've contrived the notion of an Address class, and that class uses a Serialisation trait and a Logging trait. These traits provided standard implementations of a coupla serialisation functions and a coupla logging functions (the implementations are pretty contrived, I have to admit).

In CFML - and obviously other languages, but I can only speak to what I do in CFML - I'd probably traditionally handle this with dependency injection, via ColdSpring in my current day job; but probably DI/1 if I was writing new code. However - TBH - I see using modules / traits / mixins as a better way to achieve the "inclusion" of stuff like logging and serialisation. I dunno.

Anyway, this is about traits in PHP not about dependency injection in CFML, so I'll move on.

Here're my traits:

<?php
// Logging.trait.php
trait Logging {

    static private $LOG_FILE = "C:/temp/dummy.log";

    static function logToFile($text){
        $ts = date("c");
        error_log("$ts $text\r\n", 3, SELF::$LOG_FILE);
    }
    
    static function logToScreen($text){
        echo "LOG: $text<br>";
    }
}

<?php
// Serialisation.trait.php
trait Serialisation {

    static function toJson($object){
        return json_encode($object);
    }
    
    static function toXml($object){
        $xml = new DOMDocument("1.0");
        
        $xmlObject = $xml->createElement("object");
        forEach ($object as $key=>$value){
            $xmlElement = $xml->createElement($key, $value);
            $xmlObject->appendChild($xmlElement);
        }
        $xml->appendChild($xmlObject);
        return $xml->saveXML();
    }
}


Notes:
  • note the trait keyword.
  • for some reason I could not determine, a trait cannot seem to define() a constant, so I've just run with a static variable and made it look like one here. I just didn't want to have a magic value buried in my code, hence hoisting it to the top.
  • date() is roughly analogous to dateTimeFormat(). But better-realised than CFML's one. A case in point is that the "c" mask means "in ISO format". Something that CFML doesn't have built in after how long it's existed for?!
  • error_log() is a very bare bones loose equivalent to writeLog(). But basically all it seems to do is append to a file, one has to format the "log" one's self (eg: adding time stamps etc). Plus not all logs are error logs, so it's not the best name for the functionality either, I think. It's just a bit jerry-built.
  • The DOMDocument class probably warrants its own blog article. It's a lot more long-winded than <cfxml>.
  • I haven't mentioned this before, although I did use the syntax in the previous article. I like the way how in PHP one can loop over a collection with forEach(), and populate both the key and value variables, as opposed to just the key as per looping over a collection in CFML. Obviously CFML - as of Railo 4.x and ColdFusion 10 there's structEach() (or Struct.each()) in which the callback receives both. And I prefer that approach that a loop statement anyhow.
At the end of the day, we've got a file with a coupla functions in it. Easy.

Now here's how one uses 'em:

<?php
class Address {
    use Serialisation, Logging {
        toXml        as private _toXml;
        toJson        as private;
        logToFile    as private;
        logToScreen    as private;
    }

    protected $streetAddress;
    protected $postalCode;
    protected $country;

    public function __construct($streetAddress, $postalCode, $country) {
        $this->streetAddress= $streetAddress;
        $this->postalCode    = $postalCode;
        $this->country        = $country;

        $this->logToFile($this->toJson($this->toArray()));
    }

    private function toArray(){
        return [
            "streetAddress" => $this->streetAddress,
            "postalCode" => $this->postalCode,
            "country" => $this->country
        ];
    }


    public function toXml(){
        return $this->_toXml($this->toArray());
    }

}

Notes:

  • the key bit is the use block. It dictates which traits to use (these are looked up via the autoload mechanism at this point, btw).
  • The syntax is a comma-delimited list of traits to use.
  • The block after that is not essential, but it allows me to control the names that the traits' functions are given in the class, and their access levels (and no doubt other things... that's what the docs are for if yer interested).
  • Here's I've changed the name of the toXml() function from the serialisation trait to be _toXml(). This is because I've already got a toXml() function in Address's own API; it just uses the Serialisation's implementation to do the actual work. This means in the Address API I have just a toXml() method, and that uses the general toXml(String) static function.
  • One thing I could not work out is how come if toJson() (for example) is defined as static in the trait, how come I reference it via $this->toJson() in the class? Should it not be Address::toJson()? I could not find anything in the docs to explain this.

Finally I've got a test file which creates an Address and concerts it to XML. Just to demonstrate everything's working:

<?php
require_once("../app_autoload.php");

$home =  new Address("56 Mulberry Way", "E18 1ED", "United Kingdom");
$homeAsXml =  $home->toXml();

echo htmlspecialchars($homeAsXml);

The output is:

<?xml version="1.0"?> <object><streetAddress>56 Mulberry Way</streetAddress><postalCode>E18 1ED</postalCode><country>United Kingdom</country></object>

And it also logs this:

2014-09-10T12:56:52+01:00 {"streetAddress":"56 Mulberry Way","postalCode":"E18 1ED","country":"United Kingdom"}

The only new thing here is htmlspecialchars() is the same as encodeForHtml().

There's a bunch of other considerations with traits: conflict resolution (when two traits have same-named functions), traits themselves can be composed of multiple traits. They can define abstract members too. It's all in the docs... I'd probably not add much by parroting it out here.

I think traits are quite handy. If one googles "php traits good or bad" one gets a lot of discussion about their potential misuse, but this is the same as with anything.

I'd be inclined to think that adding functionality like this into CFML might be quite a good thing..? I've raised tickets: RAILO-3215 and for ColdFusion: 3832140.

Anyway, that's long enough for me to feel comfortable with publishing. So I'll leave that here.

I continue the series with "Looking at PHP's OOP from a CFMLer's perspective: namespaces".

Righto.

--
Adam