As the title says, this is the third part of a series. The first two parts are here:
- Looking at PHP's OOP from a CFMLer's perspective: classes, objects, properties
- Looking at PHP's OOP from a CFMLer's perspective: inheritance, static & abstract, interfaces
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 ofgetDirectoryFromPath()
.__FILE__
(yes, two underscores either side. Nice one, PHP) is the equivalent to CFML'sgetCurentTemplatePath()
.
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 todateTimeFormat()
. 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 towriteLog()
. 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 thekey
andvalue
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'sstructEach()
(orStruct.each()
) in which the callback receives both. And I prefer that approach that a loop statement anyhow.
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 atoXml()
method, and that uses the generaltoXml(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 beAddress::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