PHP: Magic Methods

I love PHP; I really do. But I have caught saying, “PHP lies” on more than one occasion. (One of my Java counterparts at work loves reminding of any such declaration.)  These statements are usually the result of some sort of odd, inconsistent or just plain poor naming choice in the language.  Magic methods are not an example of this; I find them to be very appropriately named.

Fun fact about me. I used to do magic… card magic to be precise.  Nothing professional, just enough to help me through the social awkwardness of the middle and high school years. I liked cards because they were easy to come by and I didn’t need a stage or an elaborate setup to “dazzle and amaze” those around me.  I was no Copperfield, but to make sure I didn’t make a fool of myself, I had to truly understand the fundamentals of up-close performances: deception, misdirection, and obfuscation.

Ironically enough, these are words that would show up a lot later in my life.  Most often from my boss and would-be mentor: “I think all that adds is obfuscation and misdirection, Chris. Maybe we can do something simpler?” I’m paraphrasing, of course.. but the point is that these can be really detrimental characteristics of code. So when I say, “Magic Methods are very appropriately named”, it isn’t quite a glowing compliment.

To be clear, I don’t mean to say that they are an abomination either. I have had some wonderful feats of programming that would’ve never been possible without Magic Methods. They are a tool which, when required, can be very powerful; but that power comes at a cost. Bottom line: If you choose to use Magic Methods, proceed with caution.

So let’s move away from the opinion and rhetoric and get into the grit…

What are Magic Methods?

First of all, if you’ve somehow stumbled onto this post and don’t know what PHP’s Magic Methods are, here’s a quick summary.  (BTW: If you want the official description, here’s a handy link to the docs.)  These are a family of methods that start with a double-underscore which you can define as part of any class to grant it mystical powers which defy the Object-Oriented reality to which it would be normally bound. Okay… that’s a little dramatic, but this is also where we run into the first oddities that you should be aware of.

PHP does not have a common base class for all classes, yet these methods can be defined for any class.  So they aren’t part of an interface and they aren’t part of a base class. They just… are. Mindfreak!  It would be more accurate to say they just are available because the truth of it is, they aren’t there until you explicitly define them.  So if you try to directly invoke a Magic Method which has not been defined, you’ll end up with an E_ERROR which halts your application:

class Foo { }
$foo = new Foo();
$a->__get('var1'); // E_ERROR: Call to undefined method A::__get()

One might argue that these are not intended to be called directly by user-land code. Yet, they can be and therefore often are.  This is, I’m sure, an artifact of them being introduced before PHP had method visibility. To change them to anything but public would be a pretty dramatic backwards-compatibility issue, so we just need to be aware and be careful of this.  I’d like to say that you should just never directly invoke them, but it is sometimes necessary. My advice is that you treat Magic Methods as private or protected and limit your calls accordingly.

The Cool Kids…

For some of the magic methods, the illusions pretty much stop there: __construct(), __destruct(), __sleep(), __wakeup(),  __clone(), and __toString().  These offer invaluable, core functionality that goes beyond sheer convenience.  I’ll even throw in the recently introduced __debuginfo(). Added in PHP 5.6, it gives us a way to get deeper visibility into our classes for debug and/or logging purposes. I literally just discovered while writing this post and it sounds pretty swell.

I’d rather see this methods attached to some sort of low-level base class like you see in some other languages (such as Java). But such is the world we live it, and I can accept that.

Overloading

Next we’ll visit a family which specializes in misdirection and obfuscation: __call(), __callStatic(), __isset(), __set(), __get(), and __unset().  PHP generalizes these into a concept they labeled as “overloading“.  Unfortunately, the world of programming already had something called “overloading”  and this ain’t it.  There are those who will defend this choice, saying that it allows for similar functionality.  I’d say that is a pretty weak argument and we’re better off just saying, “Yeah, we were young… we didn’t yet understand the importance of consistent vocabulary in the general programming world.” and moving on with life.

Arguably poor naming aside, the family of “overloading” Magic Methods provides a very intriguing, and frankly, useful set of functionality.  You can create the illusion that there are properties or methods defined, even though there are not. I find very little use in __callStatic() and __call(), and don’t really care for them for the same reasons that I don’t care for __invoke() as detailed later. However, there a few cases where I’ve found the property-related Magic Methods to be invaluable trick to getting results which are otherwise unachievable.  Most notably, is the ability to create properties which are exposed to the outside world as either read-only or write-only. That’s neat and unique functionality. Bravo!  These rare and novel use cases are outside the scope of this post, but they do exist.  Unfortunately, what I’ve seen these most often used for is making things “easier”, “faster”, or “more convenient”.

Sometimes it appears as a proxy to an internal “data” array:

// Inside Class
public function __set($name, $var) {
    return $this->_data[$name] = $var;
}

public function __get($name) {
    return isset($this->_data[$name]) ? $this->_data[$name] : null;
}

// Outside world
echo $foo->var1;

This may look easy, but to what end? PHP already supports dynamic properties, so what have you really accomplished but squelch the warnings and notices that PHP would give you if you accessed an undefined property.  That’s exactly what you want, you say?  Then why obfuscate that? Why not just expose an ArrayObject via a getter:

// Inside class
public function __construct() {
    return $this->_data = new ArrayObject();
}

public function getData() {
    return $this->_data;
}

// outside world
echo $foo->getData()['var1'];

// More verbose way...
$fooData = $foo->getData();
echo $fooData['var1'];

This is a much more direct way of doing the same thing.  It is no more or less documentable than the “magical” way. It is no more or less “secure”, either.  The only real difference between these two ways is that the first will be a lot harder to maintain. because it is not obvious what is going on. Instead of clearly stating that you are working with an arbitrary data set, you are misdirecting your future self. This is misdirection is especially good at creating confusion when the magic happens in a parent class and even more so if it is two or three levels up the class tree.

Another common (mis)use of the magic __get() and __set() involves a switch statement and looks something like this:

public function __get($name) {
    switch($name) {
        case 'left':
            // return left value
        case 'bar':
            // return right value
        case 'sum':
            // Yay, recursion! Enjoy the trace...
            return $this->foo + $this->bar; 
        default:
            // Mimic native PHP behavior for missing property. Sort of.
            trigger_error(E_USER_NOTICE, "Undefined property: $name");
    }
}

Please don’t do this. Especially if you are doing it because  it’s  easier/faster/more efficient than writing getters. It doesn’t scale well.  What happens when there are 10 properties? Servers are fast these days, but think of all the extra processing that has to happen. Think about how difficult that will be to maintain.  Even reading stack traces on Exceptions becomes a (more) tedious operation. Trust me, you are not doing yourself any favors. Write getters and Document them with PHPDoc.  Your future self will thank you. (Pro-tip: Your IDE probably has a “generate setters/getters” tool. If not, switch to PHPStorm.)

Now, if you omit the use of __get() or __set() in the above examples, you get into that unique world of read-only and write-only public properties.  That’s very useful in a few rare cases. My advice? Before you go down this route, please consider using getters without setters or vice-versa.  It is, once again, much more direct. It is also much easier to document and maintain. If you still feel like “overloading” is the best choice, use the @property, @property-read, @property-write, and @method PHPDoc tags on your class-level PHPDoc.

The Unexplained…

So here’s another one that I just discovered while writing this post (even though I’ve read the docs a dozen times and its been available since PHP 5.1): __set_state().  I have to say… I’m a little baffled by this one.  It certainly seems to employ the magical craft of “deception”.  I find myself challenged to find a use case which isn’t better solved in another way. Maybe one of you can illustrate one in the comments section?

The Outcast…

Last, and in my opinion, least useful: __invoke().  On the surface this seems pretty neat. You can use it to make a class executable as if it was a function.  That means that you can use a class as a callback rather than using a named method of the class:

// Somewhere out there...
function doStuff(callable $helper) {
    $helper();
}

// Without __invoke()
class Foo
{
    public function stuffHelper() {
       // Helper stuff.
    }
}

// Ugly and hard for IDE's to identify as a usage of 'Foo::stuffHelper()'
doStuff([$foo, 'stuffHelper']);

// With __invoke()
class Bar
{
    public function __invoke() {
        // Helper Stuff
    }
}

// Nice and clean. Beautiful code is good code.
doStuff($bar);

Okay… looks good on the surface.  But what happens when we need arguments? Don’t worry, __invoke() has you covered. Define it however you want. You need 1 arg? No problem.  Need 12 args? Sure thing!  Need to return a string? Go ahead. How about a resource? No sweat.  It can be whatever you need it to be… Fantastic! Or not. This flexibility is actually at the core of my distaste for it.  This Magic Method adds absolutely no value and actually prevents a fundamental best-practice.

One of the universally agreed-upon best practices is having meaningful names for your arguments, variables, methods, and identifiers in general.  This simple rule transcends virtually all languages and technologies. It’s actually something that DBAs and developers see eye-to-eye on. That’s rare. Yet, __invoke() takes that away from us. And what does it give us in exchange? Examining the differences above…. very, very little.

Really, really hate that array version of specifying a callback?  I get you. As a matter of fact, I really don’t even like using callable as a argument type.  Thankfully, we can solve both our problems with interfaces.  If you aren’t familiar, I highly recommend you read up on them. Explanation of interfaces is way outside the scope of this post, but here is an example of how to use them to accomplish the same thing:

/**
 * Implementers of this interface are capable of helping with stuff.
 */
interface StuffHelper
{
    /**
     * Helps with the given stuff.
     * 
     * @param Stuff $stuff The stuff to help with.
     *
     * @return boolean A value of `true` is returned if this method 
     *                 successfully helped with stuff; otherwise a value of
     *                 false is returned.
     */
    public function helpWithStuff($stuff);
}

function doStuff(StuffHelper $stuffHelper) {
    return $stuffHelper->helpWithStuff($this->_stuff);
}

Class Foo implements StuffHelper
{
    public function helpWithStuff($stuff) {
        // Help with stuff and return success indicator.
    }

    // Other methods ...
}

// Abracadabra!
doStuff($foo);

// Unfortunately, we can no longer do this...
doStuff(function($stuff) {
    // Anonymous helper stuff.
);

Geez, that’s a lot more work.  Not really, assuming you documented your __invoke() method in the same way and now things are much more obvious and predictable, thus easier to debug, maintain, and extend.

This isn’t without its trade-offs, though. As you can see in the sample, we have lost our ability to send an anonymous function into doStuff().  Creating an implementation of StuffHelper every time we need a new way to “help with stuff” can be tedious and wasteful if you only need it in one place because that means a new class file which leads to more disk i/o, more memory usage, etc. (PHP 7 Anonymous Classes will take care of that problem, btw).  While this is all interesting, none of it justifies use of __invoke() because it also requires a class.

To sum it all up…

Magic Methods are cool, and the draw to them is strong. If you need them, then use them. But if there is another reasonable way to accomplish your goals, I strongly advise you travel that road instead, even it means a bit more typing. (Look into your IDE’s code generating capabilities and templates to help with that!)

Remember that you can not delve into magic without employing some degree of deception, misdirection and/or obfuscation… any of which may come back to bite you later.

Epilogue

Thank you for sticking in there and making it to the end.  I’ve seen the topic of magic methods fly by on reddit from time to time and I felt it was time to share my personal experiences and thoughts. As always, I fully expect you to have your own opinions based on your own experiences on the subject, and look forward to hearing about them in the comments below!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s