Wednesday 6 August 2014

Looking at PHP's OOP from a CFMLer's perspective: classes, objects, properties

G'day:
Once again I find myself looking at PHP. As an exercise, I'm gonna get up to speed with how OO is effected in PHP, and as a learning aid I'm gonna document it as I go. I hasten to add I am very much a PHP newbie so if I was you, I'd take what I say with a grain of salt. And if yo see anywhere I go wrong: please let me know.

I am working through the docs on the PHP website: "Classes and Objects", but I won't necessarily be commenting on it in order; rather more as I find out stuff, I'll jot it down. And I'll probably need to enlist the help of Google and Stack Overflow too.

Classes


So a basic class looks like this:

<?php
// Simple.class.php
class Simple {
    
}

Notes:

  • the file extension .class.php is not significant; it's a standard I've seen used a bit in the examples I've been reading, so decided to run with it.
  • There is no correlation to the name of the file and the name of the class. I can have any PHP code in there, indeed I can continue on to have procedural code in the same file, eg:

    <?php
    // Simple.class.php
    class Simple {
        
    }
    
    $simple = new Simple();
    print_r($simple);
    

    Which outputs:

    Simple Object ( )


    So I'm declaring the class and using it in the same file. I think I'd be inclined to stick to one class per file, and name the file for the class though.

Objects

Creating an object is fairly familiar:

<?php
//simple.php
require "./classes/Simple.class.php";

$simple = new Simple();

print_r($simple);

The first line includes the class file before I use it. PHP has two different concepts: include-ing and requiring. Using require or require_once differs from include-ing in that if the file is missing, an include only raises a warning, but processing continues, whereas with require, processing halts with an error. I think this is a pretty daft distinction to make... it should just error out.

The last line outputs the object. print_r just outputs a string representation of a given object. There are other ways of doing this, which I'll get to later. But this outputs:

Simple Object()

Autoloading classes

One hurdle I could foresee early in the piece is that I'm buggered if I'm gonna have separate require calls for every class I want to use. This is one benefit CFML's application-server based approach has: one can have mappings to tell the code where files are. As PHP is just file/request-centric, that's not a possibility. However it does have a concept of autoloading, which makes life easier.

There's a whole swag of material out there about the following concepts:

  • __autoload() (really, PHP? Not content with one underscore - which is already a shit thing to have in a function name - we've now got two? You suck);
  • spl_autoload_register()

The bottom line seems to be that __autoload() is an older and now deprecated way of doing autoloading, and spl_autoload_register() is the better way.

It took me a bit to get my brain around this stuff, but after I read a coupla clear articles on it, I had the eureka moment. These ones helped:


I distilled that down to a basic implementation:

<?php
// app_autoload.php
spl_autoload_register(null, false);
spl_autoload_extensions('.class.php');
spl_autoload_register(function($class){
    include "./classes/" .$class . ".class.php";
});

And then require that file:

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

$simple = new Simple();

print_r($simple);

On the face of it that's swapping one include for another, but that one include will deal with all my classes from now on.

What that app_autoload.php file is doing is:

  • clearing out any previously set autoload registrations (probably egregious in this situation, but recommended in a coupla examples I saw);
  • telling the autoloader which file extensions I want spl_autoload_register() to care about. This is where having a different file extension for classes comes in. I can have my normal script files in the same directory as class files, and the autoloader will know to ignore them.
  • giving spl_autoload_register() a callback which locates a class being autoloaded. In this case all I'm doing is telling it to include a file of the same name as the class being loaded, within the classes subdirectory. This callback could contain any logic at all, provided ultimately it includes the file. Thinking about it, I should probably require the file, not include it.
I actually quite like this approach to locating and including files.

Constructors

More underscores. The constructor for a class is a method with name... wait for it... __construct(). Yeah. With the two underscores.

<?php
// Simple.class.php
class Simple {

    public function __construct() {
        echo "Constructor was called<br>";
    }
    
}

When we run simple.php now, we can confirm the constructor was indeed run:

Constructor was called
Simple Object ( )


Note that - unlike CFML - it's not necessary to return this from the constructor.

Whilst here, PHP classes also have destructors. These are used to tidy up anything that needs tidying up when an object is itself deleted. This could be an open file stream or some such. I don't think it's something I'd be using too often. Here's a demo though:

<?php
// Simple.class.php
class Simple {

    public function __construct($name) {
        $this->name = $name;
        echo "Constructor was called for object " . $this->name . "<br>";
    }

    function __destruct() {
        echo "Destructor was called for object " . $this->name . "<br>";
    }
    
}

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

$first = new Simple("First Object");
echo "<hr>";

$second = new Simple("Second Object");
$second = null;
echo "<hr>";

Result:

Constructor was called for object First Object


Constructor was called for object Second Object
Destructor was called for object Second Object


Destructor was called for object First Object


What we see here is that setting an existing object to null destroys it, also other objects are destroyed at the end of the request.

Properties and visibility


I'll switch to another more "real world" class now:

<?php
// Person.class.php
class Person {

    public $firstName;
    public $lastName;
    private $fullName;


    public function __construct($firstName, $lastName) {
        $this->firstName = $firstName;
        $this->lastName = $lastName;
        $this->setFullName();
    }

    private function setFullName(){
        $this->fullName = $this->firstName . " " . $this->lastName;
    }

    public function getFullname(){
        return $this->fullName;
    }

}

<?php
// person.php

require_once("app_autoload.php");

$boy = new Person("Zachary", "Cameron Lynch");
print_r($boy);
echo "<br>";

echo "First Name: " . $boy->firstName . "<br>";
echo "Last Name: " . $boy->lastName . "<br>";
echo "<hr>";

echo "exposed properties:<br>";
foreach($boy as $key => $value){
    echo "{$key} => {$value}<br>";
}
echo "<hr>";

echo "Full Name via accessor: " . $boy->getFullName() . "<br>";

This outputs:

Person Object ( [firstName] => Zachary [lastName] => Cameron Lynch [fullName:Person:private] => Zachary Cameron Lynch )
First Name: Zachary
Last Name: Cameron Lynch


exposed properties:
firstName => Zachary
lastName => Cameron Lynch


Full Name via accessor: Zachary Cameron Lynch


That's all fairly obvious. The access levels are different from CFML though:

ModifierAccess
PrivateOnly the current class
ProtecedOnly the current class and other classes in its inheritance hierarchy
Public (default)Everything

This sounds good until one reads this, from the docs ("Visibility"):
Objects of the same type will have access to each others private and protected members even though they are not the same instances. This is because the implementation specific details are already known when inside those objects.
(my emphasis). What the actual f***? That's a bit stupid.

That aside, the approach to private and protected access is more "normal" than CFML's, wherein "private" is actually more like "protected" in CFML, and true "private" doesn't exist.


That's a reasonable place to stop, I think. I'll get onto stuff like static properties and methods, inheritance and abstract classes and methods next: "Looking at PHP's OOP from a CFMLer's perspective: inheritance, static & abstract, interfaces".

Righto.

--
Adam