PHP's overly compliant subclassing

| 1 Comment

Please shake your head in sympathy at this bit of terrible design in PHP's class handling. The code below is a simplified sample version of some real code I was working on last night.

$ cat foo.php
<?php

error_reporting( E_ALL ^ E_STRICT );

class Dog {
    protected $_bark;

    function __construct() {
        $this->_bark = 'Generic woof';
    }

    function speak() {
        print $this->_bark . "\n";
    }
}

class Chihuahua extends Dog {
    function set_bark() {
        $this->_bark = 'Yip yip';
    }
}

$doggie = new Chihuahua();
$doggie->speak(); // Generic bark
$doggie->set_bark();
$doggie->speak(); // Specialized bark
?>

$ php foo.php
Generic woof
Yip yip

That's about what you'd expect, right? Call a method in the subclass to modify something in the parent class, and then print it out. Nothing goofy, right?

Last night, I spent at least an hour figuring out why my code was still printing "Generic woof" instead of "Yip yip." Finally, I tried printing out my $doggie object with print_r, PHP's dumper mechanism, and it all became clear.

$ php foo.php
Generic woof
Generic woof
Chihuahua Object
(
    [_bark:private] => Generic woof
    [_bark] => Yip yip
)

It turns out that at some point I had made $_bark private in the Dog base class, thus breaking the ->set_bark() method. What is so infuriating is that instead of telling me that I was trying to modify a private class member, PHP decided to make a separate class member that Chihuahua could see, different from the member in the base class. It created a class member that I did not declare myself.

I'd love to know the logic behind this design decision. Best I can figure, it was to allow future modifications of base classes without causing name conflicts, but as far as I'm concerned, silently letting people do The Wrong Thing, even with warnings maximized is exactly the wrong behavior. PHP's tendency to silently ignore problems has always frustrated me.

As programmers, we should optimize for telling other programmers that something is wrong, rather than sweeping it under the carpet. Naked Perl doesn't do this, of course, but that's why we have warnings and strict.

1 Comment

As far as PHP is concerned, an attribute that is private has 0 visibility to inheriting classes, that is, it doesn't exist at all.

Also, assigning to an undefined attribute automatically causes it to create, as if the object were a hash.

So

$this->_bark = 'Yip yip';

Becomes analogous to

$self->{_bark} = 'Yip yip';

in perl.

However, to the inherited functions, it can still see _bark because they see it as if it were contained in a closure, so "$this" is not really the same item in both class definitions.

To do this this brokenly in perl, classes would have to be not directly related to each other, and with some hidden variable that controlled ancestry, and with AUTOLOAD falling back when a method did not exist, and then manually looking that up on some *other* object somewhere that was initialised in parallel with the first, where it sees the original value.

Logically, the way php works, it would be Impossible to throw an error, because the scope you called it on didn't have that variable defined, and you were just doing something that was accepted behaviour, dynamically creating a property ( PHP4 compatible behaviour , before "public" , "private" and "protected" were added to the language, where all fields are treated as public by default ).

Granted this is a stacked failure due to multiple rationality failures and conceptual design failures, and as a result, its 100% unfixable without introducing a significantly backwards incompatible change.

Leave a comment

Job hunting for programmers


Land the Tech Job You Love, Andy Lester's guide to job hunting for programmers and other technical professionals, is available in PDF, ePub and .mobi formats, all DRM-free, as well as good old-fashioned paper.