Wednesday 10 June 2015

PHP: more dumping in PHP... via Twig this time

G'day:
A week or so ago I was messing around with the dBug class that Kwaku Otchere (according to the author info in the class) came up with a coupla years ago: "PHP: I find the PHP equivalent of <cfdump> on GitHub shortly after I package it myself". That was an interetsing exercise (a mostly pointless one in the end, but hey). But for it to be useful for us at work, we need to be able to dump stuff from a Twig file as well as from a PHP file. Now I know one can extend Twig and add... things to it... but other than knowing where the docs are, I didn't know anything further.

I'm writing up work I did last week, btw... it's not so fresh in my mind now.

The first challenge I had was that there was a slight bug in dBug. Actually I've found a few, but this one was a show-stopper. I dunno if you recall, but I was autoloading it via composer.json:

"autoload": {
    "psr-4": {
        "dump\\": "src/"
    }
},

That works fine in a stand-alone PHP file. However I was getting a warning notice when I used dBug within my test application code:

Notice: Undefined offset: 1 in D:\blogExamples\otherLanguages\php\php.twig.local\vendor\ospinto\dbug\dBug.php on line 109


The dump was still working, but I didn't want bloody notices spewing out on my screen.

Aside:

PHP, your way of handling exceptions / errors really fricking sux. If something goes wrong... raise an exception. Don't just put a bloody message on the screen (on the screen!!!) and keep going! And this is before I get to the matter of un-catch-able runtime errors, and exceptions which can't be caught. You muppets.

Cough. Anyways...

OK, so what's the story here? The code in question is this:

//get variable name
function getVariableName() {
    $arrBacktrace = debug_backtrace();

    // [...]

    if(isset($arrFile)) {
        $arrLines = file($arrFile["file"]);
        $code = $arrLines[($arrFile["line"]-1)];

        //find call to dBug class
        preg_match('/\bnew dBug\s*\(\s*(.+)\s*\);/i', $code, $arrMatches);

        return $arrMatches[1];
    }
    return "";
}

(reminder here... Not. My. Code)

This function goes back through the backtrace (like the file listing from the debug output in CFML debugging), and locates the actual file making the dBug call, and extracting what the variable name is being dumped. I'm not really sure in the merit of this, but this is what it's doing. Or, as will happen with modern code: not happening. The problem is the regex pattern:

\bnew dBug\s*\(\s*(.+)\s*\);

And here's my code, calling dBug:

new \dBug($target);

Spot the problem? It looks like whoever wrote dBug wasn't aware of namespacing in PHP. Because I'm calling this code from one namespace, I need to qualify references to classes that don't have a namespace with a backslash. However the pattern being used doesn't expect that. It only expects the literal string "new dBug". The fix is simple:

\bnew\s+(?:.*\\)?dBug\s*\(\s*(.+)\s*\);

Basically I now expect any sort of whitespace between new and dBug (as long as there's at least one bit of it), and there's an optional namespace declaration there too (any characters at all, ending with a backslash just before the dBug). I've put a pull request in to have that fixed.

In the mean time, I'm going to use my fork of the dBug repo instead:

"repositories": [
    {
        "type": "vcs",
        "url": "https://github.com/adamcameron/dBug"
    }
],


OK, so having done a composer update on that, my PHP test of dBug works fine. Now I need to work out what to do to make it work in a Twig file.

Look through the docs (linked to further up), I think just creating a custom Twig function will be all I need. And it's dead easy in theory (this is their docs example):

$twig = new Twig_Environment($loader);
$function = new Twig_SimpleFunction('function_name', function () {
    // ...
});
$twig->addFunction($function);

All the function needs to do is to call dBug normally:

$function = new \Twig_SimpleFunction('dBug', function ($obj) {
    new \dBug($obj);
});
$twig->addFunction($function);

Easy. I needed to integrate this into my Silex site though, and follow our usual service-provider-based approach. In my Application.php I have this:

function registerProviders(){
    $this->register(new Silex\Provider\ServiceControllerServiceProvider());
    $this->register(new Silex\Provider\TwigServiceProvider(), [
        "twig.path" => __DIR__ . '\..\views'
    ]);
    $this->register(new Silex\Provider\UrlGeneratorServiceProvider());
    $this->register(new provider\service\Services());
    $this->register(new provider\service\Controllers());
    $this->register(new provider\service\ControllerProviders());
}

The key bit here being the TwigServiceProvider bit. That loads Twig.

Part of this registering the TwigServiceProvider sticks a Twig instance in $app['twig'], so in my own Services service provider I can just put this:

// Services.php
namespace me\adamcameron\twig\provider\service;

use Silex;
use me\adamcameron\twig\controller;

class Services extends Base {

    public function register(Silex\Application $app) {
        $app['twig'] = $app->share($app->extend('twig', function($twig) {
            $function = new \Twig_SimpleFunction('dBug', function ($obj) {
                new \dBug($obj);
            });
            $twig->addFunction($function);
            return $twig;
        }));
    }
}

That's about the best place for it, I think? I thought about registering all of Twig in here, but thought it'd be messy, and there's really no point. I'm OK with registering Twig itself in the usual way, and then adding my extensions separately, in my own service provider.

From there it was now a matter of writing a test. Here's my controller:

// Extension.php
namespace me\adamcameron\twig\controller;

use Silex\Application;
use Symfony\Component\HttpFoundation\Request;

class Extension {

    public static function doDump(Request $request, Application $app){

        $target = ['tahi', 'rua', 'toru', 'wha'];
        echo '<h4>Dumped from PHP code</h4>';
        new \dBug($target);

        return $app['twig']->render('extension/dump.html.twig', [
            'target' => $target
        ]);
    }

}

And my twig:

{# dump.html.twig #}
<h4>Dumped from Twig code</h4>
{{ dBug(target) }}

Here I call dBug from both the PHP code and the Twig code, and the way to do it is analogous in both situations, and pretty simple from Twig. Nice.

Oh... the output:


That's fine.

I've noticed a coupla bugs with dBug already:

  • It won't dump closures
  • Or SimpleXML objects
  • The other arguments it takes beyond just the object to dump don't work
  • And adding a label argument wasn't going to be straight forward due to the way it's coded.

I might have a look at these as a side project.

However for dumping objects and arrays, it's just fine, so that's gonna be a help to us.

The last step in the puzzle is that in our environment, I only want the dBug stuff to load in the dev environment, and not be available at all in production. This is easily done with composer.json:

"require": {
    "silex/silex": "~1.2",
    "twig/twig": "~1.0",
    "symfony/twig-bridge": "2.3.*"
},
"require-dev": {
    "ospinto/dbug": "dev-master"
},
"autoload": {
    "psr-4": {
        "me\\adamcameron\\twig\\": "src/"
    }
},
"autoload-dev": {
    "psr-4": {
        "dump\\": "src/"
    }
}

Note I have separate -dev and "non-dev" requirements and autoload sections. By default Composer will install / load everything, but there's a -no-dev option one can use with composer update which we can use in production to not install dev stuff. Nice!

I'm finishing this in a bit of a rush as I need to crack on with work, and I'm running out of time to proofread this before I need to get back to it. So I'll leave you there.

Righto.

--
Adam