This is the fourth part of a series. The first three 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
- Looking at PHP's OOP from a CFMLer's perspective: traits
Off-topic preamble
This is a bit of a departure from working through the PHP OO docs, which - for some reason - do not include namespacing in their topics. I'm looking at this now as I need to start following our coding standard (don't get me started... spaces for tabs... braces on the wrong line... grumble: PSR-2 Coding Style Guidelines), which specifies classes need to be namespaced. OK.Part of getting up to speed with the coding standard is getting the PHP Code Sniffer configured on my IDE, which I've been toiling with over the last coupla days. Getting it to work on Sublime Text on Windows was a real ball-ache, with not a single place online which details all the steps needed, nor documents the settings properly, as far as I can tell. I was going to write the process up, but as I never got it working 100% properly, I shelved that idea. It was mostly OK, except every time it ran it kicked off a PHP process which never completed. Given it runs every time I save, this killed my machine fairly quickly. I couldn't find anyone else having this, so I just surrendered.
The guys in our head dev office use PHPStorm, so I've installed the trial of that, and it was really easy to get PHPCS running on that, and the product as a whole seems more polished than Sublime Text. So I guess I'm switching IDEs.
Update:
I just got the PHPCS fixer working too... so not only doesn't it highlight coding standard "violations", it also fixes as much as it can as well. I've got this bound to my "save", so whenever I save it fixes the code first.
Anyway: namespaces.
Namespace basics
Namespaces seem a bit odd to me, coming from CFML. If I want to namespace my CFCs, I stick 'em in a package and am done with it. Provided I provide the location of the CFC's package either via a dotted path or an import statement, the CFML compiler knows what to do.In PHP it seems the file system location is separate from the namespace path... but at the same time the PSR0 coding standard says class's location in the file system needs to match its declared namespace. So it seems like a bit of a waste of time to me. And it makes PHP code look even uglier. I did not think that was possible.
I've contrived a situation that is thus:
-
namespaces/
-
me/
-
adamcameron/
-
colours/
- Factory.class.php
-
days/
- Factory.class.php
-
colours/
-
adamcameron/
- app_autoload.php
- basic.php
-
me/
colours
and days
, and each of them have a factory class for making a colour or a day. The point here being there's two classes called "Factory".In CFML we'd just have
me.adamcameron.colours.Factory
and me.adamcameron.days.Factory
and be done with it. In PHP one also has to namespace it. Well when I say "has to", I mean "if one wants to adhere to this coding standard".How to do the autoloader for this was doing my head in (mostly due to the paucity of information on how to deal with namespaces in autoloader functions), so I grabbed one from online and am pretty much just using that. So not much of this is my own code:
<?php
// app_autoload.php
spl_autoload_register(null, false);
spl_autoload_extensions(".class.php");
spl_autoload_register(function ($className) {
$className = ltrim($className, '\\');
$fileName = '';
$lastNsPos = strrpos($className, '\\');
if ($lastNsPos) {
$namespace = substr($className, 0, $lastNsPos);
$className = substr($className, $lastNsPos + 1);
$fileName = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
}
$fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.class.php';
if (is_file($fileName)) {
return require $fileName;
}
throw new Exception("$className was resolved to $fileName which was not found");
});
Observations:
- For the purposes of this exercise, I've dropped the support for
.interface.class
and.trait.class
from this (see previous articles in this series) - I'm a bit cautious about there possibly being leading whitespace in the
$className
value. I'll need to look at whether this is actually necessary, or the author just has this common - if inappropriate - habit of trimming strings within functions. strrpos()
is not just a Scottish version ofstrpos()
, it returns the position from the end of the string. I dunno how one is supposed to gleam that info from the function name. I guess the second R stands for "reverse" or something. Bloody awful function-naming there.substr()
is the same asmid()
.str_replace()
(oh, yes, go the underscores) is kinda likelistReplace()
, except - thankfully - takes arrays not lists.- it's nice having a
DIRECTORY_SEPARATOR
constant though. That's one win for PHP in this code.
Logic:
- The
$className
argument receives the fully-qualified class name, with namespace, if a namespace is specified - The rest of the logic breaks down the value into namespace and class name...
- ... infers a directory name from that...
- ... and includes the file.
$className
includes the namespace if there is one. The rest of it is just a slightly more complex version of what I had before.OK, so that'll load things in with namespaces. What about the classes and the calling code?
<?php
// Factory.class.php
namespace me\adamcameron\colours;
class Factory
{
private static $colours = ["Whero","Karaka","Kowhai","Kakariki","Kikorangi","Tawatawa","Mawhero"];
private $colour;
public function __construct()
{
$this->colour = self::$colours[rand(0, count(self::$colours)-1)];
}
public function get()
{
return $this->colour;
}
}
And similarly:<?php
// Factory.class.php
namespace me\adamcameron\days {
class Factory
{
private static $days = ["Rāhina","Rātū","Rāapa","Rāpare","Rāmere","Rāhoroi","Rātapu"];
private $day;
public function __construct()
{
$this->day = self::$days[rand(0, count(self::$days)-1)];
}
public function get()
{
return $this->day;
}
}
}
Notes:
- The important bit is the
namespace
statement. with its lovely backslashes. I am beginning to think the person/people who "designed" PHP either had absolutely no sense of the aesthetic, or simply went out of the way to create the ugliest looking code in existence. - Also note that namespace can be a block-level construct. I suppose this is so difference classes in the same file can occupy different namespaces? I guess I should try that.
- The rest of the code just kinds a random element (colour or day),
- and has an accessor function for it. I did say it was a contrived example.
rand()
is the same asrandRange()
.
And this is called from:
<?php
// basic.php
require_once "app_autoload.php";
$colour = new \me\adamcameron\colours\Factory();
$colourAsString = $colour->get();
echo "Colour: $colourAsString<br>";
$day = new \me\adamcameron\days\Factory();
$dayAsString = $day->get();
echo "Colour: $dayAsString<br>";
(it outputs the colour and the day. I'll spare you the example).Shrug. OK. Fair enough. It's a bit of an inelegant approach to things, but it works.
Multiple namespaces
What about trying two classes with the same name but different namespaces in the same file?I've created a third class file in another directory adjacent to the other two:
/me/adamcameron/numbers/Factories.class.php
.<?php
// Factories.class.php
namespace me\adamcameron\numbers\maori {
class Factory
{
private static $numbers = ["tahi","rua","toru","wha","rima","ono","whitu","waru","iwa","tekau"];
private $number;
// [...]
// otherwise identical to the previous ones
// [...]
}
}
namespace me\adamcameron\numbers\english {
class Factory
{
private static $numbers = ["one","two","three","four","five","six","seven","eight","nine","ten"];
// [...]
// otherwise identical to the one above
// [...]
}
}
Notes:
- As observed, I've omitted some of the code here: it's the same as the other factory classes above.
- I've got two namespaces within the one file here, allowing me to create two classes with "the same" name
And the test code:
<?php
// sharedFile.php
require "me/adamcameron/numbers/Factories.class.php";
$maoriNumber = new \me\adamcameron\numbers\maori\Factory();
$maoriNumberAsString = $maoriNumber->get();
echo "Maori number: $maoriNumberAsString<br>";
$englishNumber = new \me\adamcameron\numbers\english\Factory();
$englishNumberAsString = $englishNumber->get();
echo "English number: $englishNumberAsString<br>";
I'm not using the autoloader here as I didn't want to monkey with it just to accommodate this edge-case example. I would not usually put more than one class in a file, so didn't want to code for this eventuality. Hence just require
-ing it by hand. Other than that, this just demonstrates accessing both classes via their namespace again.So I guess that's good to know about, if not to actually use.
Aliasing and importing namespaces
Pleasingly, one can get rid of all that bloody awful backslash stuff, and use an alias instead. And one can also do the same with importing actual classes too:<?php
// use.php
require "me/adamcameron/numbers/Factories.class.php";
use me\adamcameron\numbers\maori as maori;
$maoriNumber = new maori\Factory();
$maoriNumberAsString = $maoriNumber->get();
echo "Maori number: $maoriNumberAsString<br>";
use me\adamcameron\numbers\english\Factory as EnglishFactory;
$englishNumber = new EnglishFactory();
$englishNumberAsString = $englishNumber->get();
echo "English number: $englishNumberAsString<br>";
Notes:- use the
use
construct to give the namespace a shorter name; - or import a class, giving it an alias.
Ooh! One thing I just noticed is that this code from the example above is unnecessarily tautological:
use me\adamcameron\numbers\maori as maori;
It could simply be this:
use me\adamcameron\numbers\maori;
(note to self: read all the docs before blogging about it ;-)
(not that I'm gonna start doing that now)
I also could have combined both use statements into one:
use me\adamcameron\numbers\maori, me\adamcameron\numbers\english\Factory as EnglishFactory;
Or perhaps this would look clearer:
use me\adamcameron\numbers\maori,
me\adamcameron\numbers\english\Factory as EnglishFactory;
Other Stuff
There's some other considerations like name-resolution rules, the ability to have dynamic alias names, and some general gotchas. These are all in the docs, and there's nothing really that interesting, so I'll not repeat it here. That's about it for namespaces. Next...--
Adam