Create a CLI for your Composer package

    I have been trying out a newly released PHP library Streetlamp the last couple weeks and it is really great. However, one feature it is missing is a way of easily discovering API endpoints for your application. The package creator is aware of this and there is an issue in the project todo that will look to rectify this but I thought I would have a tinker to see what I could come up with. Maybe I’ll submit it as a PR if my solution is any good but that is yet to be seen.

    My idea was to give the package a lightweight CLI so upon running something like ./vendor/bin/streetlamp routes we could get an output of all of the application’s routes that are registered on controllers via PHP attributes. Having never implemented a Composer package of my own, I had some learning to do to get this up and running. In this post, I am going to share the very basics of getting something like this implemented.

    Bootsrapping a package skeleton

    There are a ton of package skeleton template repos out there to choose from if you are building packages; However, for this demo we will just create a directory somewhere on our host machine and navigate to it before running composer init (assumes you have PHP and Composer installed) and following the prompts. Once this is done, we should have a composer.json and composer.lock file in our directory.

    Your composer.json will look a little something like;

    {
        "name": "alextheobold/composer_packages_blog_post",
        "description": "A demo repo for my post on creating CLIs for Composer packages",
        "license": "MIT",
        "authors": [
            {
                "name": "theoboldalex",
                "email": "theoboldalex@gmail.com"
            }
        ],
        "require": {},
        "autoload": {
            "psr-4": {
                "theoboldalex\\ComposerPackagesBlogPost": "src"
            }
        }
    }
    

    Creating the executable binary

    In order for Composer to know you want to run part of your package as an executable binary, we are required to add a property to our composer.json config. This is simple enough and if you want to go deeper into this you can Read The Friendly Manual.

    So let’s add that! Our config should now look like this;

    {
        "name": "alextheobold/composer_packages_blog_post",
        "description": "A demo repo for my post on creating CLIs for Composer packages",
        "license": "MIT",
        "authors": [
            {
                "name": "theoboldalex",
                "email": "theoboldalex@gmail.com"
            }
        ],
        "require": {},
        "autoload": {
            "psr-4": {
                "theoboldalex\\ComposerPackagesBlogPost": "src"
            }
        },
        "bin": ["bin/my-demo-cli"]
    }
    

    Notice the new line we added

    {
        "bin": ["bin/my-demo-cli"]
    }
    

    This tells composer that when our package is installed, we want to symlink the file at bin/mydemo-cli to the /vendor/bin directory and make it executable. Also worth noting is that the bin key holds an array so we can simply list any further scripts we want Composer to link to /vendor/bin.

    Implementing the CLI script

    With the setup and config out of the way, we can get on with creating the CLI.

    Note: I use the term CLI very loosely here. For this demo, we are only going to create a basic script; However, you can go as deep as you like into this and have the script take arguments and flags

    We need to create a file in the location we declared in the composer.json bin array so let’s do that.

    md bin
    vim bin/my-demo-cli
    

    Inside the file, let’s make a toy script which will simply show us the current time in a few major cities across the globe.

    #!/usr/bin/env php
    
    <?php
    
    $locations = [
        'New York' => getDateTimeByTimeZone('America/New_York'),
        'London' => getDateTimeByTimeZone('Europe/London'),
        'Tokyo' => getDateTimeByTimeZone('Asia/Tokyo')
    ];
    
    foreach ($locations as $location => $time) {
        echo sprintf("%-10s %s\n", $location, $time);
    }
    
    function getDateTimeByTimeZone(string $timeZoneString): string {
        return (new DateTimeImmutable())
            ->setTimezone(new DateTimeZone($timeZoneString))
            ->format(DateTimeInterface::RFC822);
    }
    

    Putting it all together

    At this stage, make sure that you have initialised a Git repository in your package directory and committed your changes. This is required before we can install and test our package in another repository. Ok, with that done, that is about it for the package itself so lets test it out.

    You can do this by creating a new project and pulling in the dev branch of your package by first specifying in your project composer.json (not the package composer.json) the following;

    {
        "repositories": [
            {
                "type": "vcs",
                "url": "../composer_packages_blog_post/"
            }
        ]
    }
    

    Next, simply install the dev branch of your package into the project with the following command

    composer req alextheobold/composer_packages_blog_post @dev
    

    If you have followed each step correctly, you should now see inside your project’s vendor directory a bin directory and inside that should be your executable binary. You can test this is working by running ./vendor/bin/my-demo-cli.

    Wrapping up

    It really is that simple to get started but this is just scratching the surface. There is so much more that you can do by taking in command line arguments and making the CLI more interactive to user input which ultimately leads to your packages being more useful and flexible to user demands.

    Good luck, and please let me know if you found this guide useful. You can find me on twitter