October 23, 2016

PHP and nested functions

What they did not tell you in college!

PHP and nested functions

Before I started back at college in August 2016 to work on my Bachelor’s degree, I was an intern at a small, local print shop working on an in-house e-commerce system. I became the maintainer, developer, and anything else for the entire system. Although I gave regular updates to my co-worker and management, I had total control of the system. I could, and did, write anything for the system anyway I wanted, however I thought was best. My hard work paid off too: in the four months I was there, I managed to clean up technical debt, implement, and rewrite so many areas of the store that it did not look the same as when I started (figuratively and literally, as I also performed a visual refresh of the entire site).

Did I mention this store was written in PHP? No? OK, I have now. As anybody who has read my stuff and interacted with me know, I do not like PHP. At all. Let me make a clear distinction here: I do not like PHP the language, not people who use it. People who use PHP are no less of programmers than e.g., C++ devs. I use PHP, so if I insulted or looked down on PHP devs, I belittle myself. That is not at all what I mean. I could really write an entire blog post on this topic. So, please understand that when I say I dislike PHP, I am focused specifically on the PHP language.

But yea, the home-grown store was implemented in PHP. No big deal. I may not like it but I can use it like any other language. I worked around the quirks and worked hard on making sure I wrote good code that behaved well and did not have unintended side-effects. Of course, there were times when the workarounds left me in utter amazement.

Like when you have nested functions.

I was working on my last major task before my last day: rewriting the entire cart. A task that could have easily taken a month was completed in 2.5 weeks, with only one minor bug caught (to my knowledge) after I left. One of the areas affected by this rewrite was the code for UPS shipping costs. Powered by an antiquated library (which I marked for replacement by a better one), I wrote a few wrapper functions to abstract the ugliness of the API. One of the functions, as I designed it, returned an stdClass instance containing the information for that estimate. However, there were two different cases when which the instance needed to be returned, each slightly different enough to the point my function had to return at two different times. Because the actions leading up to the first return were the same leading up to the second, I decided I would write a small nested function to construct the instance. You know, DRY.

This quickly created a problem. When the parent function was called a second time, I was presented with the following error:

Fatal error: Cannot redeclare _makeShippingObj() (previously declared in ups-shipping-wrapper.php:352) in... on line...

Well now. That is just lovely. I expected this to work. After all, it is legal in other languages! I deliberately made this a nested function because it only needs to reside in the parent’s function scope and not anywhere else (basically, I wanted to pull a JavaScript closure). What is up here?

Creating a simple test case, I was able to identify what was going on.

<?php
    function hello() {
        function name() {
            return 'Your name';
        }
        return 'Hello ' . name();
    }
    
    echo hello();  // 'Hello Your Name'
    echo name();   // 'Your Name'
    echo hello();  // Error from above

Ah, I see. It seems that, despite declaring the function instead another function, PHP is hoisting the child into the global scope, and since you cannot redeclare existing functions, that error is produced.

Well then. How do I go about fixing this?

Obviously I could move the child function out of the parent, since it technically exists there, but then I lose the meaning of why I nested it to begin with. In the end, I ended up wrapping the child in a function_exists condition to prevent any attempts of re-declaration and wrote a nice little comment explaining why. Had this blog post existed when I worked this, I would have left a link to it for the next developer to help prevent a headache they might get from the fact I even had to write the code that way in the first place.

<?php
    function hello() {        
        // Grrrrr....
        if (!function_exists('name')) {
            function name() {
                return 'Your name';
            }
        }
        return 'Hello ' . name();
    }

    echo hello();  // 'Hello Your Name'
    echo hello();  // 'Hello Your Name'

I never learned this in college or by reading a book co-written by the creator of PHP. It took real-world experience to figure it out. That is why I have written this blog post for you: so you may be aware of Yet Another Headache™ of PHP and know to solve it if you or a friend ever come across this behavior. :)

Happy PHPing! 😄

Update: after posting this, a reader sent me this message, which brings out a good point that I believe is worth sharing.

a cleaner option imho would be to use an anonymous function assigned to a  local variable. assuming you had a reasonably modern php version (if  any PHP version could be considered modern)

$name= function() use ($localDataWeNeed) { … }
return ‘hello’ . $name();

Featured image: https://commons.wikimedia.org/wiki/File:Php_elephant_logo.svg