Saturday 7 January 2017

PHP: looking into usage of the ::class constant

G'day:
There's not much to this article. It's arisen from the fact there's a paucity of docs on this subject on the PHP website, and they're quite hard to google for (given Google still insists on ignoring punctuation in searches, for some daftly ignorant reason. So searching for "php ::class" just gets one results for "php class". Not the same thing. Similarly seaching for "php ::class constant" doesn't help.

For the record, the two relevant docs pages are:


Update:

Kalle Sommer Nielsen has been in touch via Twitter and has pointed me to the original RFC for ::class, too: Request for Comments: Class Name Resolution As Scalar Via "class" Keyword. Now to be honest, the content of that RFC would make for better documentation than the current docs furnish. Obviously it'd need a slight tweak to reword some of the questions as statements, and if there were any changes between RFC and what was adopted, those should be included too. Kalle also said he's tweaked the docs slightly too, which is cool. Nice work fella!

I include those there as much for me to be able to easily re-find them as anything else.

In truth there's not much to this ::class construct. I just wish the docs were easier to locate. When discussing the feature, it's nice to be able to point to some docs. Or now: a blog article ;-)

So what does it do?

All classes have a built-in constant (::class) that contains a string that is the class's name. Same as what the get_class function will return for an object, but it works on the actual class too (because it's a class constant). Here are some examples:

<?php

namespace me\adamcameron\cc;

use com\example\other\SomeClassToAlias as AliasedClass;
use com\example\other\SomeClassToAlias;
use com\example\other\SomeOtherClass;

require_once realpath(__DIR__ . '/../vendor/autoload.php');

echo "Using ::class" . PHP_EOL;
printf("SomeClass: %s%s", SomeClass::class, PHP_EOL);
printf("SomeOtherClass: %s%s", SomeOtherClass::class, PHP_EOL);
printf("SomeClassToAlias as AliasedClass: %s%s", AliasedClass::class, PHP_EOL);
printf("SomeClassToAlias: %s%s", SomeClassToAlias::class, PHP_EOL);
printf("PHPUnit_Framework_Exception: %s%s", \PHPUnit_Framework_Exception::class, PHP_EOL);

$someClass = new SomeClass();
$someOtherClass = new SomeOtherClass();
$someAliasedClass = new AliasedClass();
$someClassToAlias = new SomeClassToAlias();
$phpunitFrameworkException = new \PHPUnit_Framework_Exception();

echo PHP_EOL . "Using get_class() on instance" . PHP_EOL;
printf("SomeClass: %s%s", get_class($someClass), PHP_EOL);
printf("SomeOtherClass: %s%s", get_class($someOtherClass), PHP_EOL);
printf("AliasedClass: %s%s", get_class($someAliasedClass), PHP_EOL);
printf("SomeClassToAlias: %s%s", get_class($someClassToAlias), PHP_EOL);
printf("PHPUnit_Framework_Exception: %s%s", get_class($phpunitFrameworkException), PHP_EOL);

In the example all the classes in the me\adamcameron or com\example namespaces are just empty classes, eg:

<?php

namespace me\adamcameron\cc;

class SomeClass {}

I've also included a PHPUnit class there as PHPUnit is a lib I'm aware of that doesn't use namespaces (dunno why), and wanted to see how that behaved, in case there were quirks: seemingly not.

Here's the output:

C:\src\php\php.local\src\oo\classConstant\src>php testWithNamespace.php
Using ::class
SomeClass: me\adamcameron\cc\SomeClass
SomeOtherClass: com\example\other\SomeOtherClass
SomeClassToAlias as AliasedClass: com\example\other\SomeClassToAlias
SomeClassToAlias: com\example\other\SomeClassToAlias
PHPUnit_Framework_Exception: PHPUnit_Framework_Exception

Using get_class() on instance
SomeClass: me\adamcameron\cc\SomeClass
SomeOtherClass: com\example\other\SomeOtherClass
AliasedClass: com\example\other\SomeClassToAlias
SomeClassToAlias: com\example\other\SomeClassToAlias
PHPUnit_Framework_Exception: PHPUnit_Framework_Exception

C:\src\php\php.local\src\oo\classConstant\src>

So you see there are no surprises really: ::class just returns the fully-qualified path of the class concerned. One thing to not is that it does not pay any attention to aliasing, whether on the class itself or on an object of that aliased class. I guess an alias is just for clarity in source code, rather than any sort of "renaming" exercise.

Where does one use ::class constants?

Well my primary usage is when mocking, using PHPUnit. Instead of this:

$mockedThing = $this->getMockBuilder('path\to\class\being\mocked\MockThisClass')
    ->disableOriginalConstructor()
    ->setMethod(['someMethod'])
    ->getMock();

We just have this:

$mockedThing = $this->getMockBuilder(MockThisClass::class)
    ->disableOriginalConstructor()
    ->setMethod(['someMethod'])
    ->getMock();

(and PHPStorm even includes the use statement for me, automatically):

use path\to\class\being\mocked\MockThisClass;

It's good to keep all the class pathing references together at the top of the file.

One flaw in this constant is this behaviour:
<?php

require_once realpath(__DIR__ . '/../vendor/autoload.php');

echo "Using ::class" . PHP_EOL;
printf("SomeNonExistentClass: %s%s", SomeNonExistentClass::class, PHP_EOL);

Any sensible person would probably want an error to be thrown there. But... no. PHP does this:

C:\src\php\php.local\src\oo\classConstant\src>php testWithNonExistentClass.php
Using ::class
SomeNonExistentClass: SomeNonExistentClass

C:\src\php\php.local\src\oo\classConstant\src>

Groan. Why does PHP insist on being "helpful" like this. The class doesn't exist! Just say that. FFS.

Oh well... that sort of thing is almost to be expected of PHP, I guess. Sigh. Well this ::class constant mostly a well-implemented, if not glamourous, small feature in PHP. And now I seem to have documented ::class more than PHP itself has. Heh.

Speaking of documentation, I can't help but think this ::class constant should also be mentioned on a coupla other pages in the PHP docs:

It'd be a good fit for both of those pages, plus that's where Google lands you if you search for it.

That's it. I have to dash to go visit my son for a few hours. Thanks to my colleague Carlos for pointing this whole thing out to me, btw. Both the ::class constant construct itself, but also the observation to be made about aliases. Nice one, fella.

Righto.

--
Adam

PS: apologies for the blatant SEO keyword stuffing of ::class constant in this article ("oops... I did it again"). It was by design.