Using Grunt with PHP Quality Assurance Tools

Showing off using Grunt and the PHP Quality Assurance Toolchain to limit code smell in PHP

The irony is not lost on me, that is certain. The language I work in the most is PHP, but with having grown a very soft spot for Grunt, I’ve been forced to learn how to use NodeJS in a more efficient manner, in particular for writing tasks and git hooks. I’ve mentioned previously how my git hooks are built, but this particular article will not be touching on that, but rather on various tasks used as wrappers around tools that exists in the PHP systems.

So, why Grunt? Why my insistance to shoehorn everything into it? Well, in my defense, it’s not everything I’m trying to shoehorn into it, but rather all tasks that should be repeatable. It’s not a case of me being too “lazy” to remember that to lint all of my files I just need to run find . -name "*.php" -exec php -l {} \; | grep "Parse error", it’s that it’s a waste of time that could’ve been spent better. Prior to finding Grunt, I would’ve either made it as an alias, or save it as a bash script, but both of those have the issue of portability. Using Grunt allows me to focus more on the actual coding — server-side or client-side — and less on ensuring that I’m checking syntax, tests, etc.

Quality Assurance Tools

Now, a disclaimer: I’ve not managed to fully implement all of these tools into my workflow. Some of them have well-written grunt tasks, and those will be noted. Others have abandoned grunt tasks, and those I will note with an alternate way of getting a more useful task until it is no longer abandoned, and the final category had no grunt tasks, so I’ve written a basic one that fits whatever options seemed likely for me as I looked over how it’s used. Those can all be found — alongside the other code for this guide — on Github, under the directory tasks. The file structure in the Gruntfile follows exactly that repository.

The set of tools I’ve implemented have all been collected from the PHP Quality Assurance Toolchain, where rather than using the PEAR Installer we will use Composer. Each tool will be installed local in the project that gets built up.

Assumptions

Obviously, you need to have installed Grunt. You also need to have cURL installed, which you probably do, especially if you followed my above-mentioned article to install Grunt, otherwise it’s still easy: sudo apt-get install curl. Finally, you’ll need to have installed Git, as the PHP project we will be working on is the basic Laravel starting page.

As far as PHP, you will need version 5.3.7+, with:

  • mCrypt
  • intl
  • XSL
  • Graphviz

sudo apt-get install php5-mcrypt php5-intl php5-xsl graphviz

Task categories

There are two specific sets of tasks that are being introduced. The first one is what I’d refer to as “blocking” tasks. They will throw an error if their conditions aren’t fulfilled, and I use them in my pre-commit git hook, ensuring that I don’t commit anything with errors in it.

The second set of tasks are probably to be ran occasionally, at whatever interval is deemed practical for your particular setup. They will just report on whatever it is they’re dealing with, and though it’s outside the scope of this guide, they should probably be used for statistic and other things. Maybe a nice set of graphs showing how the complexity of the project grows over time?

Setup

As mentioned above, the base for the PHP (because I don’t want to write a whole bunch of PHP just to showcase tasks) is going to be Laravel. Go to whatever directory you’re wanting to work in and clone the repository (I’m assuming the same name of the repository as mine: phpqatools-grunt): git clone git@github.com:laravel/laravel.git phpqatools-grunt. Be aware that not everything is set up properly just yet, we’ll get to the final bits of it further down.

You may also want to run git remote rm origin to remove the knowledge that the project has of the Laravel repository it is clone from. I at least find the “Your branch is ahead of ‘origin/master’ by …” message to be untidy.

Gitignore

Open .gitignore and replace the current one with the following:

/bootstrap/compiled.php
/vendor
composer.phar
.DS_Store
Thumbs.db
*.log
/node_modules

.*
!.gitignore
!.jshintrc
!.bowerrc
!.editorconfig
!.travis.yml
!.gitattributes
!.htaccess
*~

Package.json

After that, we’ll set up the NodeJS part of the project required for Grunt. If you run npm init it will guide you through a set of options, or you can just create a new file called package.json in the root of the project, with the following content:

{
  "name": "phpqatools-grunt",
  "version": "0.0.0",
  "description": "Showing off using Grunt and the PHP Quality Assurance Toolchain to limit code smell in PHP",
  "private": true,
  "scripts": {
    "test": "grunt test --verbose"
  },
  "repository": {
    "type": "git",
    "url": "git://github.com/Melindrea/phpqatools-grunt.git"
  },
  "author": "Marie Hogebrandt",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/Melindrea/phpqatools-grunt/issues"
  },
  "devDependencies": {
    "time-grunt": "~0.2.3",
    "load-grunt-tasks": "~0.2.0",
    "grunt": "~0.4.2",
    "grunt-jsvalidate": "~0.2.2",
    "jshint-stylish": "~0.1.4",
    "grunt-contrib-jshint": "~0.7.2",
    "grunt-jsonlint": "~1.0.4"
  }
}

As you can see, it includes a couple of basic dependencies, most of which are there to check that the various config files are valid. Two particular sections I want to draw your attention to closer:

"scripts": {
    "test": "grunt test --verbose"
  }

Once you have a grunt file, if you run npm test, it will run the grunt test task verbosely. Probably not too useful in itself, but it allows for hooking the project into Travis or similar CI that automatically runs npm test.

"private": true

This is pretty much what it says on the can. It ensures you don’t accidentally post it onto NPM until you’re good and ready.

Finally, run npm install to pull in all the dependencies from NPM. This might take a while, like all installs.

Gruntfile

Create a file called Gruntfile.js in the root, with the following content:

'use strict';

// # Globbing
// for performance reasons we're only matching one level down:
// 'test/spec/{,*/}*.js'
// use this if you want to recursively match all subfolders:
// 'test/spec/**/*.js'

module.exports = function (grunt) {
    // show elapsed time at the end
    require('time-grunt')(grunt);
    // load all grunt tasks
    require('load-grunt-tasks')(grunt);
    grunt.loadTasks('tasks');

    var directoriesConfig = {
        composer: 'vendor',
        composerBin: 'vendor/bin',
        reports: 'logs',
        php: 'app'
    };

    grunt.initConfig({
        directories: directoriesConfig,
        jshint: {
            options: {
                jshintrc: '.jshintrc',
                reporter: require('jshint-stylish')
            },
            all: [
                'Gruntfile.js'
            ]
        },
        jsvalidate: {
            files: [
                'Gruntfile.js'
            ]
        },
        jsonlint: {
            files: [
                '*.json'
            ]
        }

    });

    grunt.registerTask('default', [
        'test'
    ]);

    grunt.registerTask('test', [
        'jsvalidate',
        'jshint',
        'jsonlint'
    ]);
};

There are two tasks registered, default and test. There’s also three configs defined, adding three more tasks: jshint, jsvalidate and jsonlint. In a full-stack environment, those would also likely affect files in the project itself, but in this one they only check the Gruntfile and the JSON config files.

One more thing needs to be added to ensure those tasks run smoothly: A .jshintrc file. For further information about the various linting options, go to JSHint’s webpage, but for now create a file in the root of the project named .jshintrc, and paste in the following code:

{
    "node": true,
    "browser": true,
    "esnext": true,
    "bitwise": true,
    "camelcase": true,
    "curly": true,
    "eqeqeq": true,
    "immed": true,
    "indent": 4,
    "latedef": true,
    "newcap": true,
    "noarg": true,
    "quotmark": "single",
    "regexp": true,
    "undef": true,
    "unused": true,
    "strict": true,
    "trailing": true,
    "smarttabs": true,
    "predef": [
        "Modernizr",
        "$",
        "jQuery"
    ]
}

If you run grunt from the root now, it will … give you an error. Whoops, Tasks directory not found!

There are three sets of tasks/modules loaded in this file:

// A module that prints out the time at the end
require('time-grunt')(grunt);

// Load all grunt tasks from NPM that starts with grunt-
require('load-grunt-tasks')(grunt);

// Load all grunt tasks that are defined locally, currently none
grunt.loadTasks('tasks');

Create the tasks directory: mkdir tasks, and after that run grunt. If there are any issues with either the Gruntfile or the configs, fix them.

Composer

As alluded to above, the installation of Laravel is not complete, nor do we have all the tools to install the tasks and such. Composer is a tool, like Bower for frontend, NPM for NodeJS and Bundler for Ruby, for dependency management for PHP. The file that handles what packages to install is called composer.json and is already defined with the dependencies for Laravel, but you need to get Composer. You can install it locally or globally and in a few different ways, where I prefer keeping it local to the project and not checking it into version control. You’ll need to run just two commands:

# Download the installer to check that you have all the settings, which in
# turn downloads the latest version of the file into the current directory
curl -sS https://getcomposer.org/installer | php

# Assuming that you didn't need to adjust any settings, run the installation of
# dependencies (you may want to get another coffee ...)
php composer.phar install

I recommend looking over the documentation to see what you can do with Composer, since this article will only be touching on it briefly. Like Bundler for Ruby, it creates a lock-file that gives which versions of the packages are installed, to ensure similar working environments. Laravel by default does not add composer.lock into version control, which is one of the changes in the .gitignore file.

Diving into the deep end

Make sure you have your terminal at the ready and your Gruntfile.js open. We will work through the tasks working off of mainly those, with the occasional new file put into tasks.

Commit tasks

PHP-lint

Let’s start with the most basic of tasks: linting! PHP comes equipped with a basic syntax check. By running php -l <path/to/file>, it will check if a given file has any syntax errors in it, such as stray lack of semicolon causing a PHP Parse error: syntax error, unexpected end of file. It’s not exactly the most advanced of features, but it’s a good failsafe before you run any other checkers.

It’s corresponding grunt task is, not too surprisingly, grunt-phplint by Jacob Gable. Below the configuration for jsonlint add the following.

phplint: {
    options: {
        swapPath: '/tmp'
    },
    all: [
        '<%= directories.php %>/**/*.php'
    ]
}

If you aren’t too well-versed in how to write JavaScript objects, each of the configs needs to be separated by a comma, but there should be no comma after the very last configuration. Of course, if you do forget, you’re likely to run the task and encounter a SyntaxError: Unexpected identifier centered on the phplint word.

Another fun thing error that you might encounter is Warning: Task "phplint" not found. Use --force to continue which is just as straightforward as it sounds, at least in this case: The task is not installed. Let’s remedy that quickly by running npm install --save-dev grunt-phplint. The --save-dev flag means that it’ll write it into the package.json file. If you want to save it as a normal dependency (rather than development) the flag is instead --save.

Finally, run grunt phplint and it’s done.

Let’s look a bit closer at the config. Other options can be found in the documentation for the task, but I explicitely set the path for where the task should cache files to /tmp. I also (like I will more further into the guide) used a variable defined higher up in the Gruntfile.js in which files to lint. I like using configurations to be able to change directories and such to whatever structure I’m using.

PHP Code Sniffer

Code sniffing always sounds so incredibly weird to me. It sounds like a personal problem, though maybe code smell is as well! At any rate, I use the PHP CodeSniffer from Squiz Labs, which per their Github page tokenises PHP, JavaScript and CSS files and detects violations of a defined set of coding standards. (However, I haven’t used it for JavaScript or CSS, and unsure on if I will. After all, Sass deals with spitting out formatted CSS, and I trust JSHint to be better for actual code issues with my JavaScript.)

To install the required package and corresponding grunt task run:

php composer.phar require --dev squizlabs/php_codesniffer:1.*
npm install --save-dev grunt-phpcs

The grunt task PHPCS is created by Sascha Galley and is a fairly good one-to-one implementation of the tool. Below phplint, add the following configuration:

phpcs: {
    application: {
        dir: '<%= directories.php %>'
    },
    options: {
        bin: '<%= directories.composerBin %>/phpcs',
        standard: 'PSR2',
        ignore: 'database',
        extensions: 'php'
    }
}

Not that I define where the binary is stored, that it should only sniff PHP-files and that it should ignore the database directory. That is in fact the app/database directory, because it looks for patterns below the directory it is running in, but that’s an aside.

Finally, the standard is PSR2. It’s my preferred coding style, but it can obviously handle any kind of standard. Now, here’s one issue with using the Laravel base to test these things out: Despite Laravel following PSR-1, the base project doesn’t. Facepalm. Run grunt phpcs to confirm this, if you want to see what kind of errors it throws.

You now have two choices:

  1. Change the standard to PSR1 and just add a namespace to the following files:
    • app/tests/TestCase.php (needs to extend \Illuminate\Foundation\Testing\TestCase)
    • app/tests/ExampleTest.php
    • app/controllers/BaseController.php
    • app/controllers/HomeController.php
    • app/models/User.php (needs to extend \Eloquent)
  2. Keep the PSR2 and go through the various issues. It’s mainly whitespace issues. This commit of the test repository has the PSR2 version, if you want to just copy the app directory from it.

Either choice, once you’ve gone through the changes (or ignored the rest of this task, up to you!), run php composer.phar dump-autoload --optimize, which will recreate the autoload files (important for Laravel).

Here’s two bonus tricks:

  • If you are prepared to let a program run through your code (I’m not fully certain I am), there’s the PHP-CS-Fixer (which even has a grunt task). As much as I like automation, though, I overall prefer to have better control over that.
  • If you only have tabs rather than spaces expand will do the trick in most *nix systems. Go into the directory where you want to do any changes and run the following command (copy all of it):

    find . -name “*.php” |while read line
    do
    expand -it4 $line > $line.new
    mv $line.new $line
    done

If you go with either of those, make sure you have everything in version control or in some other way have backups.

PHP-Unit

Because you do write unit tests that you run with every commit, right? Right? … Yeah. Me too. At any rate, the Laravel project comes pre-equipped with a test unit test, so let’s get the PHP Unit up and running with grunt.

First, remove the phpunit.xml file at the root. You can keep it (the task will read from it), but at least part of what I’m wanting to do here is to gather configurations in one format, so for that reason, remove it. Now install PHP Unit:

php composer.phar require --dev phpunit/phpunit:3.7.*
npm install --save-dev grunt-phpunit

While we’re waiting, what is PHP Unit? It’s one of the primary unit testing frameworks for PHP, with the main contributor being Sebastian Bergmann. It’s a bit too large to cover in the scope of this article, obviously, but there are some very good tutorials, and the site has good documentation. The task is maintained by Sascha Galley, and is like the above-mentioned grunt-phpcs task a good implementation. All options that one could write in the phpunit.xml configuration file can be handled in the Gruntfile.

Below phpcs add the configuration:

phpunit: {
    classes: {
        dir: '<%= directories.php %>/tests'
    },
    options: {
        bin: '<%= directories.composerBin %>/phpunit',
        bootstrap: 'bootstrap/autoload.php',
        staticBackup: false,
        colors: true,
        noGlobalsBackup: false
    }
}

The bootstrap is which file it should load before running the tests, and the dir is obviously where the classes are stored.

Run grunt phpunit, which should give you a green line with the text OK (1 test, 1 assertion)

Final words

That was the last of the blocking, pre-commit tasks. To get all of them together, add phplint, phpcs and phpunit to the test task and run grunt. It should pass, if not, fix it before the next section. Or, you can just copy the files from this commit, if you prefer that.

Reporting tasks

These tasks are the ones that go through the code and write some kind of report over what it finds. I’ll admit that I’m not as familiar with most of them, and they don’t seem to be as commonly used. The default directory for all of these reports is logs/<taskname>, though it can obviously even be outside of the root. Maybe using grunt-gh-pages or a similar task to push the results to a different repository entirely.

The first thing to do here is to install two helper tasks. One is grunt-contrib-clean, which is a task to delete all files under a specific directory, and/or delete a specific directory. It’s part of the core plugins for GruntJS, as evidenced by the contrib part of the name. To install: npm install --save-dev grunt-contrib-clean

The second one is written by Sindre Sorhus and is called grunt-shell. Not too surprisingly, it allows for running shell commands. They’re fairly flexible in what you can do with them, and if what you’re needing is a few fairly simple and straightforward shell commands, it’s quite useful. One use I’ve gotten out of it is running a series of update commands; Rather than needing to run npm update && bower update && bundle update && pip install -r "requirements.txt". To install: npm install --save-dev grunt-shell

PHP Docs

I like documentation. I don’t like writing documentation, but I do like documentation. Especially in a larger team (and a larger project), I like the idea of being able to easily run one command to generate a good, cohesive overview of the project and its’ classes and dependencies. Since this is mainly an article on Grunt, I will use more-or-less default options, and leave any tweaks to the templates etc for your own choices. For more information on phpDocumentor go here.

To install with Composer, run php composer.phar require --dev phpdocumentor/phpdocumentor:2.*. As a warning, it’s fairly big and has a lot of requirements. It’s why you need XSL support for your PHP, and Graphviz. The resulting documentation is pretty rad, though.

Now, documentation is one of the things that I find to be least useful to keep all of it, so the final phpdocs task will start with cleaning out the old files and then move into generating the documentation. You’ll want to add logs/phpdocs/phpdoc-* to the .gitignore-file if you’re planning on checking in the documentation into any kind of version control system.

There is actually a task for the documentor. It’s called grunt-phpdocumentor, but the last activity was 8 months ago. There are a couple of forks of it, but none that has been published to NPM, and the latest version there has some debilitating bugs. The solution I’m going to be introducing is to copy in the relevant file (phpdocumentor.js) into the tasks directory. You will also need to install lodash when using this task: npm install --save-dev lodash.

Choose which task you want to install from the ones below.

Original task
curl -o tasks/phpdocumentor.js https://raw.github.com/gomoob/grunt-phpdocumentor/master/tasks/phpdocumentor.js
Sascha Galley’s fork adds support for template
curl -o tasks/phpdocumentor.js https://raw.github.com/SaschaGalley/grunt-phpdocumentor/master/tasks/phpdocumentor.js
My own fork adds support for ignore
curl -o tasks/phpdocumentor.js https://raw.github.com/Melindrea/grunt-phpdocumentor/master/tasks/phpdocumentor.js

Now, for the Gruntfile. Start with copying in the cleaning config wherever you feel it’s convenient:

clean: {
    phpdocumentor: '<%= phpdocumentor.dist.target %>'
},

Then copy in the phpdocumentor config after phpunit:

phpdocumentor: {
    dist: {
        bin: '<%= directories.composerBin %>/phpdoc.php',
        directory: '<%= directories.php %>',
        target: '<%= directories.reports %>/phpdocs',
        ignore: [
            '<%= directories.php %>/database/*'
        ]
    }
}

The cleaning task (grunt clean:phpdocumentor) removes the directory, the phpdocumentor task (grunt phpdocumentor) goes through all the files under the app directory (except database) and reports on what it finds. The files all go into logs/phpdocs, as per that configuration.

Finally, let’s add a new task, phpdocs, which first cleans the directory and then regenerates the docs. Add the following bit of code under the test task.

grunt.registerTask('phpdocs', [
    'clean:phpdocumentor',
    'phpdocumentor'
]);

Run grunt phpdocs and bask in the glory of shiny documentations! Bask, I tell you!

PHP Loc

Per its’ homepage, phploc is tool for quickly measuring the size and analyzing the structure of a PHP project. It’s one of the tools I’m still trying to wrap my head around, so this very first version of the grunt task is going to utilize grunt-shell. The theory behind this is that we want to have something that steadily reports on the growing complexity (most likely consumed by a cron job somewhere — possibly ran by a cron job somewhere). A really cool example from the main readme spits out a CSV file with the rows being tied to a repository. I chose not to use that one in my command, but before getting ahead of ourselves, let’s install it: php composer.phar require --dev "phploc/phploc=*".

I am likely to eventually write a task for it, but using the shell task works good for the beginning usecase, which will just write an XML-file with the results into logs/phploc/<the current datetime>.xml, based on the files in app.

Below the clean config, paste in the shell config:

shell: {
    phploc: {
        command: [
            'mkdir -p <%= directories.reports %>/phploc',
            'php <%= directories.composerBin %>/phploc --log-xml <%= directories.reports %>/phploc/<%= grunt.template.today("isoDateTime") %>.xml <%= directories.php %>'
        ].join('&&')
    }
},

Notice the use of grunt.template.today(), which is what names the file. The second thing to note is that I’m actually joining two commands. That is because if the directory doesn’t exist, the file isn’t saved where it should be.

Finally, add another task below phpdocs:

grunt.registerTask('phploc', [
    'shell:phploc'
]);

The main reason I’m adding that task is so that I don’t use the shell syntax more than necessary, as I often use the shell task for a quick-and-dirty version of tasks I intend to actually write later.

Run grunt phloc for the very first XML file.

Sensio Labs Security Checker

Though this does not replace doing your own research, it’s at least a good idea to implement some kind of automatic check that your components aren’t Naughty (boy I wish there was something like this written for bower as well, but I know how much effort goes into it, so it’s not something I can do, at least not at the moment). Alright, now that we’re past that parenthesis, do check out their web page, which has at least a touch more details.

Let’s start by installing the requirement using Composer: php composer.phar require --dev "sensiolabs/security-checker:1.3.*@dev".

This also uses grunt shell, so add another target below phploc:

securityChecker: {
    command: 'php <%= directories.composerBin %>/security-checker security:check',
    options: {
        stdout: true
    }
}

Note the option of stdout: true, that means that it will print the output to the console. You could obviously save the result, but I at least feel it would be more effort than it’s worth. If I end up finding something with a known vulnerability, I might rework it so that it logs something if it finds it.

Finally you’ll want another task, which I’m going to name vulnerability, since the theory here is that we might eventually get that frontend check that I mentioned earlier, or other things to check the vulnerability:

grunt.registerTask('vulnerability', [
    'shell:securityChecker'
]);

PHP Mess Detector & PHP Depend

PHP Mess Detector is a spin-off on PHP Depend. Both projects have (or so I have been assured by StackOverflow) their place in the reporting. PHPMD also depends on having PHP Depend installed, which is why the two tasks are written under the same header. I will start with PHP Mess Detector, since it has a grunt plugin written by Andreas Lappe.

First, install the two dependencies in composer:

php composer.phar require --dev "phpmd/phpmd:1.4.*"
php composer.phar require --dev "pdepend/pdepend:1.1.*"
PHP Mess Detector

The grunt plugin is on NPM, so to install it, run npm install --save-dev grunt-phpmd. The configuration I’ve decided on is the following, paste that in below the configuration for phpdocumentor:

phpmd: {
    application: {
        dir: '<%= directories.php %>'
    },
    options: {
        rulesets: 'codesize,unusedcode,naming',
        bin: '<%= directories.composerBin %>/phpmd',
        reportFile: '<%= directories.reports %>/phpmd/<%= grunt.template.today("isoDateTime") %>.xml'
    }
}

I have chosen to do one target that checks for all three things it might find problematic, and then stores the resulting file in logs/phpmd/<todays datetime>.xml. If you do run the task immediately, you’ll notice that it complains about the directory not existing. My solution is to add another helper plugin, grunt-mkdir. As a note, I could do what I did when I had to create a directory for phploc, which was to use the shell directly. The reason I’m not is because I’m not using the shell for phpmd, and there is a task available to install (written by Ruben Vermeersch). Run npm install --save-dev grunt-mkdir, and then paste in the configuration below the shell configuration:

mkdir: {
    phpmd: {
        options: {
            create: ['<%= directories.reports %>/phpmd']
        },
    }
},

So, since automation is the name of the game, we want to ensure that grunt mkdir:phpmd gets ran before phpmd. To do this, I will register a task phpmdMk that runs first the mkdir:phpmd and then phpmd.

Below the added task vulnerability add the following:

grunt.registerTask('phpmdMk', [
    'mkdir:phpmd',
    'phpmd'
]);

You can now run grunt phpmdMk, and it will act as expected.

As a final note, there is a discussion on the phpmd plugin on whether it should be a blocking task. At currently it will return success no matter what the result of the detector is, but there are discussions on adding a configuration to view any errors reported as a failure of the task, at which point it could, theoretically, be put in a pre-commit hook as well.

PHP Depend

There is no task for PHP Depend. I may decide to write one for myself once I’ve actually figured out what I want to do with all that nice data, but until then I wrote a small function that uses the shell task.

It takes the current ISO DateTime and creates a directory logs/pdepend/<datetime>. In that directory the summary is put (named summary.xml), a JDepend chart is created, named chart.svg, and finally an overview pyramid (pyramid.svg) is created.

Paste in this target below the securityChecker target of the shell task:

pdepend: {
    command: function () {
        var now = grunt.template.today("isoDateTime"),
        directory = '<%= directories.reports %>/pdepend/' + now,
        mkdir = 'mkdir -p ' + directory,
        summary = directory + '/summary.xml',
        chart = directory + '/chart.svg',
        pyramid = directory + '/pyramid.svg',
        pdepend;

        pdepend = 'php <%= directories.composerBin %>/pdepend '
        pdepend += '--summary-xml=' + summary + ' ';
        pdepend += '--jdepend-chart=' + chart + ' ';
        pdepend += '--overview-pyramid=' + pyramid + ' ';
        pdepend += '<%= directories.php %>';

        return mkdir + ' && ' + pdepend;
    }
}

This shows another part of the flexibility of the shell task, though this particular one isn’t very flexible in the sense of allowing dynamic calls to it.

Finally, add the task grunt pdepend below the phpmdMk task:

grunt.registerTask('pdepend', [
    'shell:pdepend'
]);

Final thoughts

Hopefully you’ve picked up some new tricks here. There were a couple of more tasks I was interested in trying to get to work,
but they weren’t cooperating with me. Maybe next time, when I’ve reworked the ones that have been defined as grunt shell targets in this article.

I’d love to hear your thoughts on Twitter, and the final version of the project built here is located on Github.


For the moment comments are not enabled, but feel free to reach out on Twitter.