How We Built Full Composer Support For Our Premium WordPress Plugins

#
By Ashley Rich

Composer is the dependency manager of choice for PHP. It allows you to declare a list of project dependencies and will install and update them directly from the command line, similar to npm for node. Back in 2015, Gilbert wrote about managing your WordPress themes and plugins using Composer. We’ve adopted a similar approach for managing the Delicious Brains site and thanks to WordPress Packagist we’re able to install the majority of plugins we require via Composer. We also update WordPress core using the WordPress Composer Fork, which means that 90% of our site is now managed by Composer.

The Composer Conundrum

That’s all fine and dandy for free plugins available on WordPress Packagist, but what about premium ones? For a long time now we’ve allowed our premium plugins to be installed via Composer, with one major caveat: only the latest version of each plugin can be installed. No matter what version you specified in your composer.json file, it would always download the latest.

Another issue with the current approach is that your composer.json file is tied to a license key (sometimes multiple keys if you’re using both WP Migrate DB Pro and WP Offload S3). On the surface, this might not seem like a big deal, but what if you’ve recently worked with a freelance developer who had access to your composer.json file and you’ve since parted ways? They now have your license key, which means they can continue to access the software that you’ve paid for. Not cool!

Although the solution we provided did technically allow you to install our plugins via Composer, it was a half-baked solution at best. But, it was still better than what most premium plugin shops provide. Most don’t support Composer at all. For those that don’t, we have to login to each site for each plugin, download the zip, unzip, replace the plugin folder, and commit the files to git. Not ideal, but it gets the job done.

Introducing Full Composer Support

As of today, I’m pleased to announce that we now offer full Composer support for our plugins. 🎉

This includes the ability to install older versions of our plugins using version constraints. In addition, we’ve completely decoupled license keys from the composer.json file via the introduction of Composer API keys. Remember that freelancer that you parted ways with? Now you can simply delete their Composer API key and they no longer have access to your software downloads.

Let’s take a look at the new workflow.

Composer Usage

We’ve added a new Settings tab to My Account where you can manage your Composer API keys. Any number of keys can be added. You can also label them for convenience.

Composer API Key

Composer API keys aren’t linked to individual licenses. Each key will grant access to all of our products for which you have an active subscription. This includes any future products we may add down the line, so you don’t need to regenerate them if you purchase a new plugin, or upgrade an existing license. 😉

Once you have an API key, you can add our repository source to your composer.json file, like so:

"repositories": [
    {
        "type":"composer",
        "url":"https://composer.deliciousbrains.com/{COMPOSER_API_KEY}"
    }
]

You can then add your desired packages and version constraints. You can use “*” as the package version to make sure you are always on the latest version of a plugin.

{
    "require": {
        "deliciousbrains-plugin/wp-migrate-db-pro": "1.8.1",
        "deliciousbrains-plugin/wp-migrate-db-pro-media-files": "1.4.9",
        "deliciousbrains-plugin/wp-offload-s3": "*"
    }
}

Running composer install should give you green across the board!

Running composer install

The plugin files are automatically installed to wp-content/plugins, but you can customize the location, if required. Check out the docs for WP Migrate DB Pro and WP Offload S3 for more details.

How We Built It

We decided to add full Composer support back in May 2017 and created a GitHub issue. Evan was our resident Composer expert, so he outlined the project requirements.

The idea would be to mimic the workflow users are already familiar with when it comes to using wpackagist.org, with the only difference being the added unique key in the Composer repository URL.

Soon after, Brad added mockups for the UI to the GitHub issue, but we decided to hold off on the implementation until the site redesign was launched. This was due to the modifications required in My Account, which would have needed to be done for both the old and new themes.

So, what were the requirements?

packages.json

For Composer to perform the package discovery, a new /packages.json endpoint was required. We chose https://composer.deliciousbrains.com/{COMPOSER_API_KEY}/packages.json, but we rewrite this endpoint in Nginx to point to our existing API.

rewrite ^/([a-zA-Z0-9]+)/packages.json$ /?wc-api=delicious-brains&request=composer_packages&composer_key=$1 last;

This endpoint needs to provide a list of packages that our repository is responsible for in addition to every available version, in the following format:

{
    "packages": {
        "vendor/package-name": {
            "1.0.0": { @composer.json },
            "1.0.1": { @composer.json },
            "1.0.2": { @composer.json }
        }
    }
}

This presented us with a problem. For us to generate this metadata we needed to know every version of our plugins that we’ve released over the years, which isn’t something we’ve ever stored in the database. Our product post type in WordPress only saves the current version available for download.

Luckily, we store all of our product downloads in a single S3 bucket, which includes all versions ever released. They’re also named in a standardized way, for example: wp-migrate-db-pro-1.0.zip. As we already use WP Offload S3 on the site, we could use the bundled AWS SDK to list the objects and extract the versions. We perform this lookup every time we update a product in WordPress, but only when the product version has changed. This data is then saved to the options table, which in turn is cached in Redis.

$objects = $as3cf->get_s3client()->listObjects( [
    'Bucket' => 'downloads.deliciousbrains.com',
    'Prefix' => 'wp',
] )->getPath( 'Contents/*/Key' );

$plugins = array();
foreach ( $objects as $object ) {
    $file = self::get_name_and_version( $object );

    if ( ! $file ) {
        continue;
    }

    $plugins[ $file['name'] ][] = $file['version'];
}

Going back to the packages.json format outlined above you’ll notice that each package has a value of { @composer.json }. This should be the information included in each plugin’s composer.json file, however we don’t ship our plugins with one. Instead, we generate this dynamically for each package.

array(
    'name'    => $name,
    'version' => $version,
    'type'    => 'wordpress-plugin',
    'dist'    => array(
        'url'  => self::get_download_url( $plugin, $version, $composer_key ),
        'type' => 'zip',
    ),
    'require' => array(
        'composer/installers' => '~1.0',
    ),
);

This is mostly self-explanatory. The url key points to another endpoint on our API (see below) where the package ZIP can be downloaded. The type and require blocks are what allow us to specify the install location for the packages. By default, they would be installed to /vendor, but using composer/installers allows the default location to become /wp-content/plugins by specifying a type of wordpress-plugin.

Download Endpoint

The download endpoint is where Composer attempts to download the requested package, once it’s been found on our packages.json endpoint. It’s here that we validate that the package is indeed accessible for the given Composer API key. If the key is attached to a user who has an active subscription for the requested package, the download will commence. Otherwise, Composer will fail and a relevant error shown.

Composer install fail

And that’s all there is to it!

If you’ve been installing our plugins via Composer previously, I encourage you to check out the new system and upgrade soon. The old system will continue to work for now, but will eventually be deprecated.

Calling All Plugin Shops

Adding Composer support to distribute your premium plugins may seem like a daunting task at first glance, but as we’ve shown here, it’s relatively straightforward. Hopefully, this article will prompt a few other plugin shops to take action, so that as a community we can improve the experience of managing premium plugins with Composer. Looking to make your must-use plugins compatible with Composer? Check out this article.

Update: We have since improved Composer support for our premium plugins again. Find out all the details in this post.

Have you ever created your own Composer repository before? Let us know in the comments.

About the Author

Ashley Rich

Ashley is a Laravel and Vue.js developer with a keen interest in tooling for WordPress hosting, server performance, and security. Before joining Delicious Brains, Ashley served in the Royal Air Force as an ICT Technician.