How to Build and Distribute Beautiful Command-Line Applications with PHP and Composer
Tags: php, composer, cli, githubactions
Published: 2022-02-12 15:55:00

When you think of command-line applications, PHP doesn't immediately come to mind. Yet the language powers many popular tools, either as independent programs or to be used within projects.

Here are a few examples:

* [Valet](https://github.com/laravel/valet)
* [Psalm](https://github.com/vimeo/psalm)
* [PHPStan](https://github.com/phpstan/phpstan)
* [PHP Insights](https://github.com/nunomaduro/phpinsights)
* [PHP CS Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer)
* [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer)
* [Rector](https://github.com/rectorphp/rector)
* [Takeout](https://github.com/tighten/takeout)

Be it through its vast ecosystem of libraries and frameworks, its ability to interact with the host, or the versatility of its dependency manager, PHP features everything you need to build and ship powerful CLI applications.

This tutorial will walk you through the process of creating a simple game running in the terminal, using [Symfony's Console Component](https://symfony.com/doc/current/components/console.html) as a bedrock, [GitHub Actions](https://github.com/features/actions) for compatibility checking, and [Composer](https://getcomposer.org) for distribution.

We won't use specialised frameworks like [Minicli](https://github.com/minicli/minicli) or [Laravel Zero](https://laravel-zero.com) because the goal is not so much to focus on features but to better understand the development, testing, and distribution phases of command-line programs.

You will find that you can build a lot of things on top of such a simple foundation, but you can always check out these frameworks later on, should you need extra capabilities.

## In this post

- Prerequisites
- Repository and core dependencies
- Entry point
- Command
- Styling the output
- Distribution test
- PHP version support
- Release
- Cleaning up
- Conclusion
- Resources

## Prerequisites

To complete this tutorial, you will need:

* A [GitHub](https://github.com) account (a free one will do)
* A [local PHP instal](https://www.php.net/manual/en/install.php)
* A [local Composer instal](https://getcomposer.org/doc/00-intro.md)
* A terminal to interact with the above

If at any time you feel lost, you can also refer to this article's [repository](https://github.com/osteel/php-cli-demo).

## Repository and core dependencies

The first thing we need to do is create a GitHub repository named `php-cli-demo` (or anything you like, but the rest of this guide will refer to this name so it might be simpler to stick to it).

You can make the repository public straight away if you want, but I like to do so only when the code is pretty much ready. No matter what you choose, I will let you know when is a good time to make it public.

![Create a new repository](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7rfhpb742cxyg81sfdoc.png)

Next, open a terminal and clone the project locally, and enter its root directory:

```
$ git clone git@github.com:<YOUR GITHUB USERNAME>/php-cli-demo.git
$ cd php-cli-demo
```

Run the following command to create a `composer.json` file interactively:

```
$ composer init
```

It will ask you a series of questions – here is some guidance on how to answer them:

* `Package name (<vendor>/<name>)`: I distribute all my packages under the vendor name `osteel`, so in my case, it's `osteel/php-cli-demo`. If you're not sure, you can just pick your GitHub username
* `Description`: shown as optional but will be required for distribution later on. You can set "PHP command-line tool demo"
* `Author`: make sure you set values that you are comfortable sharing publicly
* `Minimum stability`: go for `stable`. You may sometimes need to pick `dev` instead if some of your tool's dependencies aren't marked as `stable` (see [here](https://getcomposer.org/doc/04-schema.md#minimum-stability) for details). That won't be the case in this tutorial though
* `Package type`: choose `library` (see [here](https://getcomposer.org/doc/04-schema.md#type))
* `License`: this is up to you, but since the code will be distributed and available publicly, it has to be open source. [MIT](https://choosealicense.com/licenses/mit) is fine in most cases, but if you need something more specific [this guide](https://choosealicense.com) will help. Make sure to set a value that is [recognised by Composer](https://getcomposer.org/doc/04-schema.md#license), too. For this article, `MIT` will do

We will set our dependencies manually, so answer `no` when asked about defining them interactively. Keep the default value (`src/`) for PSR-4 autoload mapping.

The last question is about adding the `vendor` directory to the `.gitignore` file, to which you should reply `yes`.

That should be it for `composer init` – it will generate a `composer.json` file at the root of your project based on the values you've selected.

Before we instal any dependency, let's create a `LICENSE` file at the top of our project and paste the content of the chosen license in it ([here's the MIT one](https://choosealicense.com/licenses/mit) – you will find the others [here](https://choosealicense.com)). Make sure to edit the placeholders for the name and year.

Let's now require [Symfony's Console Component](https://symfony.com/doc/current/components/console.html) as a dependency:

```
$ composer require symfony/console
```

![composer require symfony/console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/poqbgqfawypmr11u1uv8.png)

This component is used as a foundation for many frameworks' command-line features such as Laravel's [Artisan](https://laravel.com/docs/artisan). It's a robust and battle-tested set of classes for console applications and is pretty much a standard at this stage, so it makes sense to use it as a starting point instead of reinventing the wheel.

Once the script is done, you should see the dependency listed in the `require` section of `composer.json` (your version might be different based on when you're completing this tutorial):

```
"require": {
    "symfony/console": "^6.0"
},
```

## Entry point

Console programs, like web applications, need an entry point through which all "requests" come through. In other words, we need an `index.php` for our commands.

To get started, we will reuse the example given in the Console Component's [documentation](https://symfony.com/doc/current/components/console.html#creating-a-console-application). Create a new folder named `bin` at the root of the project and add a `demo` file to it, with the following content:

{% gist https://gist.github.com/osteel/1e856c4f4cfc7037c8a96ecb68cd97ee %} 

The `demo` file will be the entry point of our program, allowing us to call it from a terminal using commands like this one:

```
$ demo play
```

If you already have a local application named `demo`, give it a different name (if you're not sure, run `which demo`  and see if it returns a path to an existing application).

Back to our `demo` file – notice the first line:

```
#!/usr/bin/env php
```

This is a way for the system to know which executable it should use to run the script, saving us from having to explicitly call it.

In other words, that's what will allow us to run this:

```
$ demo play
```

Instead of this:

```
$ php demo play
```

`#!/usr/bin/env` means the system will use the first `php` executable found in the user's `PATH`, regardless of its actual location (see [here](https://unix.stackexchange.com/questions/29608/why-is-it-better-to-use-usr-bin-env-name-instead-of-path-to-name-as-my) for details).

Speaking of execution, this will only work if `demo` is executable. Let's make sure that is the case:

```
$ chmod +x bin/demo
```

Now run the following commands:

```
$ php bin/demo
$ ./bin/demo
```

Both should display the default console menu:

![Default help menu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ka0cij5kaei7fteavtc.png)

## Command

Now that the base structure of our application is in place, we can focus on the command that will contain the core of our program. What we're building is a simple game of calculation, giving the user basic sums to solve and returning whether the answer is correct or not.

Once again, the [documentation](https://symfony.com/doc/current/console.html) provides some guidance on how to write commands, but for the sake of simplicity, I will give you the code straight away, followed by a breakdown.

Create some new folders `src` and `src/Commands` at the root of the project and add the following `Play.php` file to the latter:

{% gist https://gist.github.com/osteel/7373c90d14568b5a5d3b1fc71a76ac6f %}

Make sure to change the namespace at the top to reflect your vendor name.

Our class extends Symfony Console's [`Command` class](https://github.com/symfony/symfony/blob/6.0/src/Symfony/Component/Console/Command/Command.php) and overwrites some of its static properties. `$defaultName` and `$defaultDescription` are the command's name and description, which will both appear in the application's menu. The former will also allow us to invoke the command in the terminal later on.

Next comes the `execute` method, that the parent class requires us to implement. It contains the actual logic for the game and receives an instance of the console's input and output.

There are several ways to interact with those and I admittedly find the official documentation a bit confusing on the topic. My preferred approach is to rely on the [`SymfonyStyle` class](https://symfony.com/doc/current/console/style.html) because it abstracts away most typical console interactions and I like the default style it gives them.

Let's take a closer look at the body of the `execute` method:

{% gist https://gist.github.com/osteel/816b97705eec486d5322defcc9c29936 %}

First, we randomly generate the two terms of the sum and calculate the result in advance. We instantiate `SymfonyStyle`, pass the console's input and output to it and use it to display the question and request an answer (with the `ask` method).

> _If you're unfamiliar with `sprintf`, it's a [PHP function](https://www.php.net/manual/en/function.sprintf) allowing developers to format strings in a way that I find easier to read than the standard concatenation operator (`.`). Feel free to use the latter instead, though._

We collect and save the answer in the `$answer` variable, cast it to an integer (inputs are strings by default), and compare it to the expected result. If the answer is correct, we display a success message; if not, we display an error.

We then ask the user if she wants to play another round, this time using the `confirm` method since we need a boolean answer ("yes" or "no").

If the answer is "yes", we call the `execute` method recursively, essentially starting the game over. If the answer is "no", we exit the command by returning `Command::SUCCESS` (a constant containing the integer `0`, which is a code signalling a successful execution).

The returned value is a way to tell the console whether the program executed correctly or not, and has nothing to do with the answer being right or wrong. These exit codes are [standardised](https://tldp.org/LDP/abs/html/exitcodes.html) and anything other than `0` usually means "failure". You can see [here](https://github.com/symfony/symfony/blob/6.0/src/Symfony/Component/Console/Command/Command.php#L36) which exit codes the `Command` class defines by default and come up with your own if you need to.

The above breakdown is more than enough for this article, but feel free to check out the [official documentation](https://symfony.com/doc/current/console.html) for details and advanced command usage.

We now need to register our command to make it available from the outside. Update the content of the `bin/demo` file to match the following:

{% gist https://gist.github.com/osteel/514a2c7e3f1eabf87803b0bb8aaa779b %}

The only thing we've changed is we now pass an instance of our command to the application before running it (again, don't forget to update the namespace of the imported class).

Go back to your terminal and run this from the project's root:

```
$ ./bin/demo
```

The help menu should display again, only this time the `play` command should be part of it, alongside its description:

![Help menu with the command](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b2z2ekfgiqvthsb6jxbf.png)

We should now be able to play the game, so let's give it a try:

```
$ ./bin/demo play
```

Make sure it behaves as you would expect – if it doesn't, go back a few steps or take a look at the tutorial's [repository](https://github.com/osteel/php-cli-demo) to check if you missed something.

![Testing the game](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xcwmctbzh0l45grtd1s1.png)

> _As our application will expose a single command `play`, we could also have made it the [default one](https://symfony.com/doc/current/components/console/changing_default_command.html). Doing so would have allowed us to start the game by running `demo` instead of `demo play`._

## Styling the output

The defaults provided by the `SymfonyStyle` class are quite good already, but these `[OK]` and `[ERROR]` messages are a bit dry and not very suitable for a game.

To make them a bit more user-friendly, let's create a new `Services` folder in `src`, and add the following `InputOutput.php` file to it:

{% gist https://gist.github.com/osteel/b2ccd0597cee8e137ac46e8db37362a8 %}

Once again, make sure to change the vendor namespace at the top.

The purpose of this class is to extend `SymfonyStyle` to adapt its behaviour to our needs.

The program interacts with the player in three ways – it asks questions and indicates whether an answer is right or wrong.

I've reflected this in the `InputOutput` class above by creating three public methods – `question`, `right`, and `wrong`. `question` simply calls the parent class' `ask` method and prepends the question with the ✍️ emoji.

For `right` and `wrong`, I drew inspiration from the parent class' [`success`](https://github.com/symfony/symfony/blob/6.0/src/Symfony/Component/Console/Style/SymfonyStyle.php#L135) and [`error`](https://github.com/symfony/symfony/blob/6.0/src/Symfony/Component/Console/Style/SymfonyStyle.php#L143) methods and adapted their content to replace the default messages with more convivial emojis.

And that's all there is to it – the only thing left is to adapt the `Play` command to use our new service instead of `SymfonyStyle`:

{% gist https://gist.github.com/osteel/dac6ef1e3e96f3956ab98faf68777070 %}

As usual, pay attention to namespaces.

Let's play the game again and appreciate the new look:

```
$ ./bin/demo play
```

![New look](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jj3y1kubye090wsrjcul.png)

Now, you might be wondering why I created new methods instead of extending the `ask`, `success` and `error` methods. The quick answer is that doing so increases compatibility across library versions.

Extending a method means the signature of the child method must be identical to that of the parent (with some [subtleties](https://www.npopov.com/2021/11/08/Type-variance-in-PHP.html)), which is fine as long as the parent class doesn't change. For the sake of portability, however, we want our application to be compatible with various PHP and dependency versions, and in the case of the Console Component, method signatures vary from one major version to another.

In short, creating new methods instead of extending existing ones allows us to avoid errors related to signature mismatch across versions.

The above is a quick example of how you can customise the style of console outputs. It also gives you an idea of how you can structure your application, with the introduction of the `Services` folder where you can put utility classes to decouple your code.

Concerning styling, if you need to push the appearance of your console program a bit further, you might want to take a look at [Termwind](https://github.com/nunomaduro/termwind), a promising new framework described as "Tailwind CSS, but for PHP command-line applications".

## Distribution test

Let's now take a few steps towards the distribution of our application. First, we need to tweak our `demo` script:

{% gist https://gist.github.com/osteel/cb70ee7dd0635983765855c1a12acbe2 %}

The bit we've updated is the following:

```
$root = dirname(__DIR__);

if (! is_file(sprintf('%s/vendor/autoload.php', $root))) {
    $root = dirname(__DIR__, 4);
}

require sprintf('%s/vendor/autoload.php', $root);
```

We're now looking for the `autoload.php` file in two different places. Why? Because this file will be located in different folders based on whether we're in development mode or distribution mode.

So far we've used our application in the former mode only, so we knew exactly where the `vendor` folder was – at the project's root. But once it'll be distributed (installed globally on a user's system), that folder will be located a few levels above the current one.

Here's for instance the path to the application's `bin` folder on my system, once I've installed it globally:

```
~/.composer/vendor/osteel/php-cli-demo/bin
```

And here is the path to the global `autoload.php` file:

```
~/.composer/vendor/autoload.php
```

The path to `/vendor/autoload.php` is then four levels above `bin`, hence the use of `dirname(__DIR__, 4)` (this [function](https://www.php.net/manual/en/function.dirname) returns the path of the given directory's parent, which here is the parent of the current directory `__DIR__`. The second parameter is the number of parent directories to go up).

Save the file and make sure you can still access the application by displaying the menu:

```
$ ./bin/demo
```

> _[Since Composer 2.2](https://getcomposer.org/doc/articles/vendor-binaries.md#finding-the-composer-autoloader-from-a-binary), a `$_composer_autoload_path` global variable containing the path to the autoloader is available when running the program globally. This variable would allow us to simplify the above script, but also means we'd have to enforce the use of Composer 2.2 as a minimum requirement, potentially impacting compatibility._

Another thing we need to do is add `composer.lock` to the `.gitignore` file:

```
/vendor/
composer.lock
```

If you come from regular PHP application development, you may be used to [committing this file](https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control) to freeze dependency versions (also called _dependency pinning_), thus guaranteeing consistent behaviour across environments. When it comes to command-line tools, however, things are different.

As mentioned earlier, your CLI application should ideally work with various PHP and dependency versions to maximise compatibility. This flexibility means you can't dictate which versions your users should instal, and instead let client environments figure out the dependency tree for themselves.

Note that you [_can_](https://getcomposer.org/doc/02-libraries.md#lock-file) commit this file – it will be ignored when your application is installed via Composer. But in practice, explicitly ignoring `composer.lock` best reflects real usage conditions, and forces you (and your team) to consider version variation early on.

By the way, if you'd like to know more about when to and when not to commit `composer.lock`, I published a Twitter thread on the subject:

{% twitter 1465285217892114443 %}

Since we don't enforce specific dependency versions anymore, we need to define the range of versions our application will support. Open `composer.json` and update the `require` section as follows:

```
"require": {
    "php": "^8.0",
    "symfony/console": "^5.0|^6.0"
},
```

We need to specify the PHP version because down the line we won't know which versions our users are running. By making it explicit, Composer will then be able to check whether a compatible version of PHP is available and refuse to proceed with the installation if that's not the case.

Right now we're only interested in running a distribution test on our current machine though, so specifying your local PHP version is enough (adjust it if it's different from the above – if you're not sure, run `php -v` from a terminal).

You'll notice that I also added version `5.0` of the Console Component – that's because while running my own tests, I experienced some compatibility issues with my local instance of Laravel Valet, which needed Console `5.0` at the time.

Based on when you're completing this tutorial, versions may have changed and this constraint may not be true anymore, but it's a good example of the kind of compatibility issues you may have to deal with.

There's one last change we need to make to this file, and that is the addition of a `bin` section:

```
"require": {
    "php": "^8.0",
    "symfony/console": "^5.0|^6.0"
},
"bin": [
    "bin/demo"
],
```

This is how you specify the [vendor binary](https://getcomposer.org/doc/articles/vendor-binaries.md), which is a way to tell Composer "this will be the application's entry point, please make it available in the user's `vendor/bin` directory". By doing so, our users will be able to call our application globally.

Commit and push the above changes. Then, if your repository was private so far, now is the time to make it public. Go to the _Settings_ tab, enter the _Options_ section, and scroll all the way down to the _Danger Zone_ (which always reminds me of [Archer](https://www.youtube.com/watch?v=k3-zaTr6OUo)).

There you can change the repository's visibility to _public_:

![Change repository visibility](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ze9yvzy992rhz9ppooqx.png)

Now head over to [packagist.org](https://packagist.org/) and create an account (or sign in if you already have one).

Once you're logged in, click the _Submit_ button in the top right corner and submit the URL of your repository. Allow a few moments for the crawler to do its job and your repository will appear:

![Crawled repository](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0vkhv78wzeavm0ela5lg.png)

Don’t worry too much about publishing a dummy package – the worst that can happen is that no one but yourself will instal it (and you will be able to delete it later anyway).

Let's now try and instal it on your machine. Go to your terminal and run the following command:

```
$ composer global require osteel/php-cli-demo:dev-main
```

Then again, make sure to set the right vendor name. Notice the presence of `:dev-main` at the end – this is a way to target the `main` branch specifically, as there is no release available yet.

Once the instal is successful, make sure that the system recognises your application (you may need to open a new terminal window):

```
$ which demo
```

This command should return the path to your application's entry point (the `demo` file). If it doesn't, make sure the `~/.composer/vendor/bin` directory [is in your system's `PATH`](https://blackdeerdev.com/how-to-add-composer-vendor-bin-directory-in-your-path).

Give the game a quick spin to ensure it behaves as expected:

```
$ demo play
```

Our distribution test is successful!

## PHP version support

Before we carry on and publish a proper release, we need to tackle something we've now mentioned a few times – PHP version support.

This will be a two-step process:

1. Write a test that controls the correct end-to-end execution of our program
2. Add a GitHub workflow that will run the test using various PHP versions

### Automated test

We will use [PHPUnit](https://phpunit.de/) as our testing environment – let's instal it as a development dependency:

```
$ composer require --dev phpunit/phpunit
```

Create the folders `tests` and `tests/Commands` at the root of the project and add a `PlayTest.php` file to the latter, with the following content:

{% gist https://gist.github.com/osteel/848328f795dc5426fc0143075da74945 %}

As usual, make sure to update the vendor in the namespace and `use` statement.

As per the [documentation](https://symfony.com/doc/current/console.html#testing-commands), the Console Component comes with a `CommandTester` helper class that will simplify testing the `Play` command.

We instantiate the latter and pass it to the tester, which allows us to [simulate a series of inputs](https://symfony.com/doc/current/components/console/helpers/questionhelper.html#testing-a-command-that-expects-input).

The sequence is as follows:

1. Answer `10` to the first question
2. Say `yes` to playing another round
3. Answer `10` again
4. Say `no` to playing another round

We then have the tester execute the command and check if it ran successfully using the `assertCommandIsSuccessful` method (which controls that the command returned `0`).

And that's it. We're not testing the game's outputs – we're merely making sure that the program runs successfully given a series of inputs. In other words, we're ensuring that it doesn't crash.

Feel free to add more tests to control the game's actual behaviour (e.g. whether it gives the correct answers), but for our immediate purpose, the above is all we need.

Let's make sure our test is passing:

```
$ ./vendor/bin/phpunit tests
```

![Test execution](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/otm80c21xpsb7m2fsxm0.png)

If it doesn't, go back a few steps or take a look at the tutorial's [repository](https://github.com/osteel/php-cli-demo) to check if you missed anything.

### GitHub workflow

Now that we have a test controlling the correct execution of our program, we can use it to ensure the latter's compatibility with a range of PHP versions. Doing so manually would be a bit of a pain, but we can automate the process using [GitHub Actions](https://github.com/features/actions).

GitHub Actions allow us to write workflows triggering on selected events, such as the push of a commit or the opening of a pull request. They're available with any GitHub account, including the free ones (they come with a [monthly allowance](https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions)).

Workflows are in YAML format and must be placed in a `.github/workflows` folder at the root.

Create this folder and add the following `ci.yml` file to it:

{% gist https://gist.github.com/osteel/54ec1379790735d900560b0ba0b60fe2 %}

At the very top of the workflow is its name ("CI" stands for [Continuous Integration](https://www.martinfowler.com/articles/continuousIntegration.html)), followed by the events on which it should trigger, in the `on` section. Ours will go off anytime some code is pushed to the `main` branch, or whenever a pull request is open towards it.

We then define jobs to be executed, which in our case is just one – running the test. We specify that we'll use Ubuntu as the environment, and then define the execution [strategy](https://docs.github.com/en/actions/learn-github-actions/contexts#strategy-context) to apply. Here, we'll be using a [matrix](https://docs.github.com/en/actions/using-jobs/using-a-build-matrix-for-your-jobs) of PHP versions, each of which will run our test in sequence (the listed versions are the [supported ones](https://www.php.net/supported-versions.php) at the time of writing).

Finally, we define the steps of our job, that we break down as follows:

1. Check out the code
2. Set up PHP using the current version
3. Instal Composer dependencies
4. Run the test

We now need to list the same PHP versions in our application's `composer.json` so Composer won't refuse to instal it for some of them. Let's update the `require` section again:

```
"require": {
    "php": "^7.4|^8.0",
    "symfony/console": "^5.0|^6.0"
},
```

If you're wondering why I didn't list `8.1`, that's because as per the rules of [Semantic Versioning](https://semver.org/) (_semver_), `^8.0` covers all versions above or equal to `8.0` but strictly under `9.0`, which includes `8.1`. If you're like me and tend to forget the rules of semver when you need them, keep this [cheatsheet](https://devhints.io/semver) handy and thank me later.

Save, commit and push the above – if it went well, you should see the workflow running in the _Actions_ tab of your GitHub repository:

![Workflow execution](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/821ooaxkdsxusnzcfg0q.png)

Then again, refer to the article's [repository](https://github.com/osteel/php-cli-demo) if something goes wrong.

This workflow gives us the confidence that our application will run successfully with any of the listed PHP versions, and will confirm whether this is still the case any time we push some changes.

> _The above is a simplified workflow that doesn't quite cover all dependency versions. We can push the concept of matrix even further, but to avoid making this post longer than it already is, I've published a [separate blog post](https://dev.to/osteel/a-github-workflow-to-check-the-compatibility-of-your-php-package-with-a-range-of-dependency-versions-11ab) dedicated to the topic._

## Release

We now have a tested application that is compatible with a range of PHP versions and an early version of which we managed to instal on our machine. We're ready to publish an official release.

But before we do that, we need to ensure our future users will know how to instal and use our application – that's what `README.md` files are for.

Let's create one at the root of the project (click "view raw" at the bottom to see the source code):

{% gist https://gist.github.com/osteel/e32b81b2063eb847a097e110c5433b0f %}

This is a basic template mixing some HTML and markdown code. I won't spend too much time explaining it, so here is a quick summary of the content:

* Title and subtitle
* Preview image
* Some badges
* Installation instructions
* Usage instructions
* Update instructions
* Deletion instructions

You'll need to replace instances of `osteel` with your vendor name again (and maybe some other things like the application name or entry point if you chose something other than `demo`).

For the preview image I like to take a simple screenshot of the application being used:

![Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ivvfite17i0xnhxb7thn.png)

Note that this is also the image I used to illustrate this post.

Create an `art` folder at the root of the project and place a `preview.png` image in it (update the code accordingly if you pick a different name).

You can also use this picture for social media by setting it in the _Social preview_ section of the repository's _Settings_ tab.

That's pretty much it – feel free to reuse and adapt this template as you please.

Save and push `README.md`, and head over to the repository's _Releases_ section, available from the column on the right-hand side:

![Releases](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wf18hlqxrkz5r4exsh6r.png)

Create a tag for your release, which should follow the rules of [Semantic Versioning](https://semver.org/). We'll make ours version 1:

![Release tag v1.0](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6cslnilf5bfbyic86or9.png)

Give your release a name and a description and publish it:

![Release name and description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hhik9eadtvxvdo60gauz.png)

After a few moments, you should see it appear on the Packagist page of your repository:

![Release on Packagist](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/knk72rimxpat0mtify9d.png)

Now go back to your terminal and run the following command (using the appropriate vendor name):

```
$ composer global require osteel/php-cli-demo
```

Now that a proper release is available, there's no need to append `:dev-main` anymore.

Make sure you can still play the game:

```
$ demo play
```

And that's it! Your application is now ready to be advertised and distributed.

You can instal future minor releases by running the following command:

```
$ composer global update osteel/php-cli-demo
```

And major ones with the following:

```
$ composer global require osteel/php-cli-demo
```

## Cleaning up

This tutorial is drawing to an end and if after playing with your application you feel like cleaning up after yourself, you can delete your package from Packagist:

![Packagist options](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vsxciiu53vce3tt7e58w.png)

And this is how you'd remove it from your local system:

```
$ composer global remove osteel/php-cli-demo
```

Of course, you can also delete your GitHub repository at any time.

## Conclusion

I hope this will inspire you to write your own command-line tools in PHP.

While the language's primary application remains for web development, the above should show that it's versatile enough to assist you in other ways.

Let's be honest – if your goal is the widespread use of a standalone CLI application, PHP is probably not the best language. Users would still need an environment that supports it, a local instal of Composer, and also to edit their local `PATH` to include Composer's `bin` directory. These are all steps that most people won't be comfortable taking.

But if you're already a PHP developer, your environment is very likely to satisfy these conditions already, and as a result, the barrier to building your own CLI tools is very low.

Think of repetitive tasks that you perform regularly, and that could be automated away without the need of a full-blown website. I, for instance, grew tired of having to manually import my Kobo annotations to [Readwise](https://readwise.io), so came up with a [tool](https://github.com/osteel/kobwise) to make the conversion easier.

PHP has no issue running [system commands](https://www.php.net/manual/en/function.system.php) and if you need more muscle than what the Console Component alone has to offer, frameworks like [Laravel Zero](https://laravel-zero.com) will greatly expand the realm of possibility.

You don't necessarily have to distribute your programs via Packagist for a strictly personal use, but it's a way to share your work with others who might have similar needs and a potential foray into open-source development.

Besides, the process of writing and distributing your own CLI tools is not so dissimilar to that of application libraries – once you're comfortable with the former, the latter becomes more accessible, and both are ways to give back to the community.

## Resources

Here are some of the resources referenced in this post:

* [Article repository](https://github.com/osteel/php-cli-demo)
* [Composer documentation](https://getcomposer.org/doc/00-intro.md)
* [Symfony's Console Component documentation](https://symfony.com/doc/current/components/console.html)
* [Choose an open source license](https://choosealicense.com)
* [Why is it better to use "#!/usr/bin/env NAME" instead of "#!/path/to/NAME" as my shebang?](https://unix.stackexchange.com/questions/29608/why-is-it-better-to-use-usr-bin-env-name-instead-of-path-to-name-as-my)
* [Appendix E. Exit Codes With Special Meanings](https://tldp.org/LDP/abs/html/exitcodes.html)
* [Type variance in PHP](https://www.npopov.com/2021/11/08/Type-variance-in-PHP.html)
* [PHP's dirname function](https://www.php.net/manual/en/function.dirname)
* [PHP Supported Versions](https://www.php.net/supported-versions.php)
* [PHPUnit](https://phpunit.de/)
* [Continuous Integration](https://www.martinfowler.com/articles/continuousIntegration.html)
* [GitHub Actions](https://github.com/features/actions)
* [Packagist.org](https://packagist.org/)
* [Semantic Versioning](https://semver.org/)
* [Semantic Versioning Cheatsheet](https://devhints.io/semver)
* [How to add composer vendor bin directory to path](https://blackdeerdev.com/how-to-add-composer-vendor-bin-directory-in-your-path)
* [Minicli](https://github.com/minicli/minicli)
* [Laravel Zero](https://laravel-zero.com)
* [Termwind](https://github.com/nunomaduro/termwind)