Skeleton Install for PHP Command Line Tools

#note2self

Thanks to Andrew Cavanagh for proofing this and Matt Lantz

Basically this skeleton tool was something I made to help out my day to day work and hopefully it will come in handy for others writing PHP Command Line Tools. You can get to the repo here.

Since I often need command line tools and like writing them with PHP — for example, see Pickle — I really wanted to remove all the boiler plate work needed to get started. PHPLeague and their Skeleton install took care of almost all of what I needed. But for the setup for these command line tools I wanted to install and use Silly, PHPUnit, DotEnv, CGR etc and put in place some “best practices” for me to feel comfortable working.

Now I know Python and Ruby are just sitting there waiting for me to use to make powerful command lines tools but I really like PHP, and there typically is an SDK for any service I want. And to top that Composer is a super pleasant experience to work with, though pip for Python has been nice to use too.

But as the next tool I needed to make came up I decided it is time to make a simpler way for me to get going on this. Here is the gist of what this Skelton, based 99% on PHPLeague Skeleton, gets me.

  • Scripted install to setup my overall package for proper Composer and Packagist use
  • A command file that has an example command to remind me how to make this stuff work
  • A bootstrap process that works in global or local install
  • An “app” file that is bootstrapped so I can keep my Dependency Injection work in there and not in the command file
  • Config Option and DotEnv options (OPTIONAL)
  • Helper File for command shortcuts (OPTIONAL)
  • An Example test to show how I can test my Classes before placing them into the final command
  • A TestCase file with “$app” loaded to extend and Mock as needed any injected Classes into my App.
  • TravisCI ready to go (OPTIONAL)
  • Scrutinzir (OPTIONAL)

Scripted Install

The goal here is one command to quickly setup my app for easy Composer install and many other features.

Thanks to the work of the PHPLeague Skeleton package one command gets you going.

php prefill.php

answer a few questions and you are ready.

One addition here is when you name the app’s vendor/repo alnutile/foo for example it will name your command foo

This foo file lives in the root of your app.

Done with the install! Now to cover all that comes with it.

Command File with Example Command

So now let’s look at the command file here and below:

as noted above this file is in the root of your app named “command” but becomes the name you gave your repo

So I start off with an a command that uses the bootstrap file (more on this later) and shows how to get started with arguments and options. As well as passing $app into the function if needed.

Another thing it has in the example is a try / catch pattern which reminds me that, like a Controller, I really do not want to leave the user seeing a 5xx error when using the command line either.

Notice the lines with the style options, “error”, “info”, “question”. Again just some nice reminders for me of some of the options I have for this.

One thing I will cover later is testing and how much of the work and logic is not in here but in the Class it calls to. This allows me to just plug in the final product here knowing it will work.

But now to cover the bootstrap setup.

Bootstrap Setup

Nothing much to see here. Just a quick include of “autoload”.

The `bootstrap/autoload.php` file as seen here and below:

Wow might even have stolen the comments from the Laravel Artisan package that I copied some of this code from :)

So I just wanted a file to deal with loading both vendor and my app. But more importantly this file deals with the fact that the user will be installing this via CGR (more on this later) or as a developer using it locally for testing.

This leads into the App file.

The App File

This file is where I build the state of the “app” including config, env settings if needed, Dependency Injection, etc.

This can be seen below and here:

This file is really key. For one it keeps a lot of the dependency injection work in one place. So I can use it in my command line file, in my Tests file (more on this later) and in other places as well if the CLI grows (maybe later I do Queue work).

Load DotEnv:

To begin with there is the load_dotenv() that I created in my bootstrap/helpers.php that I will cover in a moment. This is very optional and can easily be removed but I will go into the details of this later on.

Load Config:

Then I load a config state I can tap into from any class.

$app->getContainer()['config'] = function() use ($app) {
return config();
};

Again config just being a helper function I made in the bootstrap/helpers.php file.

All of this is also totally optional.

For example I will show in the test how this can be used to easily access `config` settings from anywhere in the code:

$this->assertEquals("boo", $skel->getConfigValueByKey('baz'));

This can be seen in the Test snippet below.

Based on what I have see Laravel do I wanted a class I can centralize some info around. So what you see above is basically is a wrapper around the Silly Application and some methods, in this case “getConfigValueByKey” for a friendlier access to this information:

Here is a code example of accessing “config” items via the core Application class, I show in this test tests/SkeletonClassTest:

$this->assertEquals("boo", $skel->getConfigValueByKey('baz'));

Which calls to getConfigValueByKey() off $skel

NOTE: $skel comes from line 17 above where I call to $this->app which is part of the TestCase setup I extend. All of this a reuse of the dependency injection work I did earlier in boostrap/app.php

And $skel variable is just a result of my original bootstrap/app.php that I call to in the test.

$app->getContainer()[\Alnutile\Example\SkeletonClass::class] = function() use ($app) {
$skel = new \Alnutile\Example\SkeletonClass();
return $skel;
};

$app->getContainer()['skel'] = $app->getContainer()[\Alnutile\Example\SkeletonClass::class];

And if you look at the SkeletonClass it just ends up extending src/Application.php.

From “Applicatoin.php” I can talk to the container in a clean way like:

public function getConfigValueByKey($key)
{
if (isset($this->getConfig()[$key])) {
return $this->getConfig()[$key];
}
return null;
}

That should show how the overall application comes together into a usable state and how you can reach into it for existing dependencies, environment variables and configuration options.

Config Option and DotEnv options (OPTIONAL)

This is something I can see myself using but it is easily removed if needed.

As seen in “bootstrap/app.php” I take a moment to create a key “config” that returns the results of the config() function. This function looks like this.

function config($path = false, $file_name = "config.yml") {
$path = ($path) ? : getcwd();

$config = [];

if(file_exists($path . "/" . $file_name)) {
/**
* Get file from path command is run in
*/
$config_yaml = file_get_contents($path . "/" . $file_name);

$config = \Symfony\Component\Yaml\Yaml::parse($config_yaml);
}

return $config;
}

This just allows me to have a config.yml file in the root app using this command line tool, if needed. In this case it is a yaml format but you could swap it out per your preference.

For example I have one app where this global command line tool will look for a yaml file and load it as needed for the user when they pass a key as an argument. For us the keys are staging , production etc and what is loaded is some secret configuration info so the user can manage tasks on those projects. The config.yml in that case is in the root directory of the app folder they are running the global command in.

Helper File for Command Shortcuts

There are functions I can call too in the bootstrap/helpers.php file.

You can see me use them above when I call toconfig() or load_dotenv. Of course these are totally optional and just ideas I have seen in Laravel and wanted to have here as well.

The file lives here “bootstrap/helpers.php” and is loaded in the bootstrap/app.php file.

require_once __DIR__ . '/helpers.php';

Add more, or just ignore it.

Just a side note, I had this in the composer.json file as:

"autoload": {
"psr-4": {
"Alnutile\\Example\\": "src"
},
"files": [
"bootstrap/helpers.php"
]
},

But the path is wrong using cgr so I backed out of this cleaner way of loading this file.

Testing

The Skeleton comes with an example test. So let’s say I have already run the prefill.php command to setup my app as noted in the readme.md once that is done and I run composer install then I am ready to run this starting test.

composer test

Note:
If you get a PHPUnit error it might be due to PHPUnit 6 being used. All you need to do is alter the extends \PHPUnit_Framework_TestCasein tests/TestCase.php to extends \PHPUnit\Framework\TestCase

Let’s see what is going on here with Testing and the example test that shows several features.

Line 17:

This shows how I can pull the class and any related dependencies right out of the container. In this case the class has no particular dependencies but if it had it would already have been taken care of for me in the bootstrap/app.php file.

Line 23:

This shows another example of me using the config state I set earlier.

Line 25:

Finally I show how to use the DotEnv class to get and set the state from a .env file.

The class itself extends TestCase, which is another nice starter feature for me as it allows me share the state of the app that I create in there. As well as other things I can do in there that other classes can benefit from.

Global Install (OPTIONAL)

Composer has a global install option and it works, but I did not want to be limited by other global packages so by using CGR I can install my command line tools without worrying about one using library 1.x.x and another using 2.x.x of some shared library. CGR already has instructions on their site on how to install it and I have posted some notes here as well.

BUT if it is a private repo I had to do this:

After you install cgr make a folder in the path where your private library will be installed. For example, let’s use my github account alnutile and a package called foo

mkdir -p ~/.composer/global/alnutile/foo

Note the global folder.

Then in there make a composer.json file with this info:

{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/alnutile/foo.git"
}
]
}

That is it, now when you run cgr global require alnutile/foo:dev-master as long as your terminal is set for private GitHub access, you are set.

Test Coverage (OPTIONAL)

This allows me to get a look at my TestCoverage and some other nice statistics about my code.

If you look in .travis.yml you will see:

- bin/phpunit --coverage-text\
--coverage-clover=coverage.clover

This will run with a coverage report like so:

This would output in TravisCI as a pass fail if the limits are below your desired limits.

You can alter these in the phpunit.xml

logging is the area of focus for right now

What you end up with is some pretty interesting reports like this:

And

All of this is inside the build folder created when I run phpunit now that I have it setup for this.

For me it has been really nice to see overall the coverage I have and learn about some areas I might be missing. Read more on how to manage Coverage here.

And since it is in TravisCI’s .travis.yml file it causes a PR to fail before merging if the levels are not up to what is needed for that project.

So even though this stuff is optional I think it is really helpful to push yourself in these areas. Also being a shared project this helps to unify the level of testing done.

Style Guide (OPTIONAL)

Lastly there is style. Again optional but as with Testing coverage it creates the level of expectations for you and others working on this project.

In this case .travis.yml has this line:

script:
- bin/phpcs --standard=psr2 src/

This will cause TravisCI to fail if there are style issues. You can run these as you work or before you push by running these commands as seen in your composer.json :

"scripts": {
"check-style": "phpcs -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src tests",
"fix-style": "phpcbf -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src tests"
}

For me I run fix-style a couple of times until it returns no errors. And then check-style to see if all is fixed. Since path where your can not be automatically fixed just review it’s output and manually fix itms as needed.

Some things to keep in mind:

  • Start from the lowest line in the file and work up since you do not want to lose your line count related to the error it notes.
  • Try a change an run again. You might learn that, for example, adding one new row per variable in a function is a good style but you did not do it correctly and it will help you to see why.
  • fix-style sometimes does not complete the first time or errors out. Just run it a few times till you get:
No fixable errors were found
Time: 64ms; Memory: 6Mb

That is about it. For all the annoying things it finds “wrong” the fixes are not that hard and the end product is a codebase that is more consist for a team to then do code reviews on.

Scrutinzir (OPTIONAL)

Last thing, there is a “.scrutinizer.yml” file in the root of your directory. So when you push your code you will see some more reporting around it.

So if you go to your GitHub repo, for example, you will see a bit more reporting. If you click on the “code quality” one you can then sign up and turn this on, for free, for public repositories.

Just click the button. login and set it up:

It will take a bit but then you get some nice results like this:

Again helping to learn a bit about your code and layers that I might not have considered.

Developer Workflow

Wow seems like a ton of work! Well all of this stuff is if you need to do something more than a simple bash command or what not. So hopefully this will tie together some of the details for building a CLI tool that is simple but can grow.

One thing I do, that helps a lot, is I work in the folder of the tool to get going e.g. ~/Code/tool

And if that needs to be used in another app I get it all working and tested and before I push it I cd into the other app folder and run. For example:

~/Code/foo <--my folder for my foo project
cd ~/Code/bar <-- my app I want to see it used in
../foo/foo setup <-- then run it from the app folder but reference the local version of `foo`

Then push and cgr update alnutile/foo just like composer

Now it is running globally on my system after working out any kinks locally.

Conclusion

And that is it! Now making commands are “easy” and as the idea grows your framework is ready to keep it well tested, well styled and ready to work with others.