Building an easily distributable single-file command line app with PHP and Composer
Tags: php, tutorial, webdev, automation
Published: 2021-01-21 13:59:04

---
title: Building an easily distributable single-file command line app with PHP and Composer
published: true
description: Learn how to quickly build a command line app that can be shared with any developer that has PHP installed on their computer
tags: php, tutorial, webdev, automation
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/dbefhzv7sjb5nrd5cef9.png
---

> This post was compiled from [two of my](https://twitter.com/pretzelhds/status/1351851922744029185) [Twitter threads](https://twitter.com/pretzelhds/status/1352217411437604865). Be sure to [follow me](https://twitter.com/pretzelhds) to see the new ones first!

---

In May 2020, Symfony released version 5.1 of their components and included something truly beautiful for us developers: The `SingleCommandApplication` class. This allows you to define and immediately run a command line app in 32 lines of code. Here it is:

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

<?php

require __DIR__ . '/vendor/autoload.php';

use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\SingleCommandApplication;

const JOKE_API_ENDPOINT = 'https://official-joke-api.appspot.com/jokes/%s/random';

function command(InputInterface $input, OutputInterface $output)
{
    $topic = $input->getOption('topic');
    $jokeResponse = file_get_contents(sprintf(JOKE_API_ENDPOINT, $topic));
    [$joke] = json_decode($jokeResponse);

    $output->writeln($joke->setup);
    $output->writeln("<info>{$joke->punchline}</info>\n");
}

(new SingleCommandApplication())
    ->setName('Joke Fetcher')
    ->addOption(
        'topic', 't',
        InputOption::VALUE_OPTIONAL,
        'Get a joke relating to a specific topic',
        'general'
    )
    ->setCode('command')
    ->run();
```

And this is what it looks like when you run it. 


![Command line app fetching two jokes](https://dev-to-uploads.s3.amazonaws.com/i/8lks7w5dof7q3mu29c6u.jpg)

Let's examine the app piece by piece.

## Dependencies

For building the command line app, we require only a single dependency from Symfony.

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

Once that's installed, you're ready to go!

## Setting the stage

The first 12 lines are all setup. We include the autoloader and all classes we need. We also setup our endpoint as a constant so it doesn't become a magic string floating around in our application code.

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

<?php

require __DIR__ . '/vendor/autoload.php';

use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\SingleCommandApplication;

const JOKE_API_ENDPOINT = 'https://official-joke-api.appspot.com/jokes/%s/random';
```

> 👀 Take note of the shebang (`#!/usr/bin/env php`) on line 1! It enables us to run our helper using `./joke-fetcher` instead of having to call PHP specifically (`php joke-fetcher`).

## Defining the command

Our actual command is just a PHP function called exactly that: `command`. Making it a separate function is a style choice. You can pass a closure directly to the `SingleCommandApplication`.

```php
function command(InputInterface $input, OutputInterface $output)
{
    $topic = $input->getOption('topic');
    $jokeResponse = file_get_contents(sprintf(JOKE_API_ENDPOINT, $topic));
    [$joke] = json_decode($jokeResponse);

    $output->writeln($joke->setup);
    $output->writeln("<info>{$joke->punchline}</info>\n");
}
```

In here we fetch the topic that the user can give us, and request our joke endpoint. We then output it with some pretty formatting.

In a real application, you'd want to use a proper HTTP client to do this. `file_get_contents`/`json_decode` is just easy for demonstration.

Also you can also learn more about console output formatting in [the Symfony docs](https://symfony.com/doc/current/console/coloring.html#using-color-styles)

### Building our `SingleCommandApplication`

This is the interesting part! We create a `SingleCommandApplication` and immediately use the return value to define various settings.

```php
(new SingleCommandApplication())
    ->setName('Joke Fetcher')
    ->addOption(
        'topic', 't',
        InputOption::VALUE_OPTIONAL,
        'Get a joke relating to a specific topic',
        'general'
    )
    ->setCode('command')
    ->run();
```

`setName` is optional. It just improves the output of some pre-delivered options (e.g. `--version`).

`addOption` allows us to add a command line flag the user can set. It takes the following arguments:

* The name of the option (`--topic`)
* A shortcut for the option (`-t`)
* A "mode" (Is the option is required, optional or boolean?)
* A description of the command (used for `--help`)
* A default value (The standard topic of our jokes)

```php
->addOption(
    'topic', 't',
    InputOption::VALUE_OPTIONAL,
    'Get a joke relating to a specific topic',
    'general'
)
```

If you're using PHP8 already: This is a great place to use named arguments to selectively set options. 

However, the optional arguments all have UX implications. It's in your best interest to set sensible values for them.

The second to last line (`setCode`) sets the code the command line app should run. This can be any kind of [PHP callable](https://t.co/FhBPOmZ0LV)

> 💁‍♂️ I like pulling out functions to avoid nesting and improve reusability

The last line (`->run()`) immediately runs the app we've just made.

And there you have it. You can now run your app and enjoy corny jokes!

If you want, you can also rename your PHP file to remove the `.php` extension and run it as `./<filename>` -- You just have to add the executable flag with `chmod`!

## Making the app distributable

To make our app easily distributable and executable, we'll have to turn it into a PHP archive, better known as a PHAR file.

For this we'll use the ✨ beautiful [`phar-composer`](https://github.com/clue/phar-composer) package by [clue.engineering](https://clue.engineering)

Installing it is a lot like installing composer, if you do it manually. The steps look like this

```bash
$ curl -JOL https://clue.engineering/phar-composer-latest.phar`
$ php phar-composer-1.2.0.phar install clue/phar-composer
$ rm phar-composer-1.2.0.phar
```

>❗️ You should always double-check what you're pulling into your shell with `curl/wget`, but I vouch for this one

Also note, that the version may not be 1.2.0 for you. The package is regularly updated.

## Preparing our composer.json

Now we need to add a `bin` property to our `composer.json` to let `phar-composer` know which file is runnable. In my case it's `cli.php` and it's in the app root folder

```json
{
    "name": "pretzelhands/joke-fetcher",
    "require": {
        "symfony/console": "^5.2"
    },
    "bin": ["cli.php"]
}
```

As a ✨quality-of-life improvement✨ I also like to add a Composer script called `phar` that just calls the necessary command for us.

So instead of running

```bash
$ phar-composer build
```

I can run

```bash
$ composer run phar
```

which is more consistent within most of my projects. To add the Composer script, edit your `composer.json` file and add a new `scripts` property like this:

```json
{
    "name": "pretzelhands/joke-fetcher",
    "require": {
        "symfony/console": "^5.2"
    },
    "scripts": {
        "phar": "phar-composer build"
    },
    "bin": ["cli.php"]
}
```

Now you can run it! In my case the `joke-fetcher` app we wrote above comes out to ~980KB.

💄 For aesthetic reasons you can also rename your PHAR to remove the file extension.

🔥 You can share this app with anyone who has PHP installed and they can just run it with chmod 755!~

## Final notes

* 🤬 `phar-composer` will complain if `phar.readonly` is set to `off` in your php.ini - This doesn't stop it from working. But it advises you how to get rid of the message

* 💬 For the name of your PHAR file, `phar-composer` looks at the `name` prop in your `composer.json`

* 🚮 `phar-composer` won't check if there's already a PHAR in the project directory. It'll just package it in there, making your PHAR twice as big

You need to delete it yourself. You can make this part of the `composer run phar` script. This is left as an exercise for the reader ;)

## Further reading

Look at these links if you want to learn more about the Symfony Console command, `phar-composer` and various details of PHP!

* 📦 `phar-composer`: https://github.com/clue/phar-composer
* 👀 PHAR files: https://php.net/manual/en/intro.phar.php
* `SingleCommandApplication`: https://symfony.com/doc/current/components/console/single_command_tool.html
* Symfony Console component: https://symfony.com/doc/current/components/console.html
* Console options and arguments: https://symfony.com/doc/current/components/console/console_arguments.html

And here's a gist, for anyone who wants to see the entire code at once!

{% gist https://gist.github.com/pretzelhands/43d0025ba958538efe371fe867a2dae6 %}

---

> If you enjoyed this post and want to read more like this, [follow me](https://twitter.com/pretzelhds) on Twitter! Or [subscribe](https://pretzelhands.com/newsletter) to my newsletter