Use Statamic's Command API to make addon installation easy

There's an easy way to avoid users having to manually move folders and files from your addon to their site. Here's how...

Published May 5th 2019

Quick summary: Learn how to use Statamic's File and Copy classes to easily move files from your addon to a user's site folder, so they don't have to do it manually.

There are a handful of addons in the Statamic marketplace that require users to manually move files, such as fieldsets, to the required location within the site folder. However, there is a way of automating this, so the user only needs to run one command, and everything will be configured for them.

The purpose of writing this article is to help other Statamic addon authors to leverage the powerful Command API. This in turn will help to make it easier for Statamic users to set up and install your addons.

Required knowledge

Though I’ll walk you through how to configure your addon to enable this functionality, it will help to have a good understanding of Laravel, Statamic’s folder structure, and object-orientated PHP.

A dummy addon

For the purpose of this tutorial, we’ll create a dummy addon we’ll call CompanyDetails. This addon will create a global and a fieldset to go along side it. At the time of writing, there aren’t really any addons that do this sort of thing. But it’s a really interesting concept for rapidly setting up and configuring Statamic in an environment where you need to build several sites a year with common functionality.

We’ll initially build the addon in small stages, starting with scaffolding out the global and the fieldset, before moving on to writing the commands that will move the files to where they need to be.

Step 1: Creating the addon folder and meta file

When I build addons, the first file I create is meta.yaml. This is where all of the information about the addon – e.g. the author, version, description, etc – is set. It’s a nice place to start, and ensures that you don’t accidentally forget to add it before publishing.

In your editor of choice, in the sites/addons/ folder, create a new file and call it CompanyDetails/meta.yaml. We’ll then add the following details:

name: Company Details
description: Adds – and allows the output – of company details such as name and address.
version: "1.0"
developer: Your name

Once you’ve created this, log into your Statamic site and go to the addons page. The Company Details addon should be visible, along with the other details we’ve added.

Step 2: Creating the global and its fieldset

The first thing we’ll do is create a global; well, the files that will power the global anyway. It won’t work while it’s sat in the addon folder. We need to move it to the globals folder (unsurprisingly) before it’ll work. But we’ll deal with that later.

To keep things as simple as possible, we’ll just add two fields for the time being. The first is name, and the other is logo. We’ll also call the global company so that when you output the global, it’ll be semantic names like {{ company:name }} and {{ company:logo }}.

To keep things organised, let’s create a resources folder within the addon, and a globals folder within that.

fieldset: company
title: Company

The ID will automatically be assigned by Statamic after it’s loaded for the first time.

Next, we’ll create the company fieldset:

sections:
  main:
    display: Main
    fields:
      name:
        type: text
        display: Name
        validate: required
      logo:
        restrict: true
        type: assets
        display: Logo
        container: main
        folder: company
        max_files: 1
        mode: grid
taxonomies: false
hide: true
title: Company

Step 3: Creating the command

Right now, we have an addon that, well, to be frank doesn’t do anything. Let’s do something about that.

The next file we’re going to create is an install command. To do this, create a file at /sites/addons/CompanyDetails/Commands/InstallCommand.php. Once you’ve done that, we’ll scaffold out the file.

<?php
/**
 * This command allows users to install the addon without manually needing
 * to move files to their required parts of the sites folder.
 * 
 * @package CompanyDetails
 * @author  Your name <your@email.com>
 * @license MIT https://opensource.org/licenses/MIT
 */
namespace Statamic\Addons\CompanyDetails\Commands;

use Statamic\Extend\Command;

class InstallCommand extends Command
{
    
}

Just to recount what we’ve done here, we’ve configured the namespace based on the folder structure, created a class called InstallCommand, which extends Statamic\Extend\Command.

Statamic’s Command class has a couple of requirements in order to work with Please, Statamic’s CLI wrapper for Artisan. These in turn are inherited from Illuminate\Console\Command.

They are $signature and $description. Of these two, the signature is the most important one, being the command that they use to initialise the command. Description on the other hand can be left blank, but it’s good practice to fill it out so users are left in no mind what it does.

<?php
/**
 * This command allows users to install the addon without manually needing
 * to move files to their required parts of the sites folder.
 * 
 * @package CompanyDetails
 * @author  Your name <your@email.com>
 * @license MIT https://opensource.org/licenses/MIT
 */
namespace Statamic\Addons\CompanyDetails\Commands;

use Statamic\Extend\Command;

class InstallCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'companydetails:install';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Installs the CompanyDetails addon.';
}

Let’s take a step back for a second and boot up a terminal. Go to the root of your Statamic project. Once you’re there, type php please and hit enter. It should return a list of available commands. Scroll down and you should see your command is available. Don’t run it for the time being though. It won’t do anything, anyway.

The image showing in the terminal. The image showing in the terminal.

Step 4: Configuring the command method

The next thing we need to do is to write the actual method that will be fired when a user types companydetails:install.

To do this, we’ll scaffold a method called handle().

/**
 * The method fired when a user runs companydetails:install
 * 
 * @return void
 */
public function handle()
{
    
}

The next step is to think about what it is we need to do. In our relatively simple example, we just need to move the fieldset and the global from the addon to their place within the site folder.

In order to do this, we need a method that can copy files from one place to another. We could create individual methods for each file, but that wouldn’t be very DRY.

As such, we’ll create a private method to do this. Let’s call it moveFiles. Within it, we’ll accept two parameters; the file we’re moving, and where we are moving it to.

/**
 * Moves files from one location to another.
 * 
 * @param string $from The location the files are currently in.
 * @param string $to The location you want to move the files to.
 * @return void
 */
private function moveFiles(String $from, String $from)
{
    
}

The next thing we need to do is to call the File class. To do this, add Statamic\API\File to your file, and then use the copy method as so within moveFiles().

/**
 * Moves files from one location to another.
 * You do not need to pass the addon name.
 * 
 * @param string $from     The location the files are currently in.
 * @param string $sitepath The folder in the sitepath to move the files to.
 * @param string $to       The location you want to move the files to.
 * 
 * @return void
 */
private function moveFiles(String $from, String $sitepath, String $to)
{
    File::copy();
}

We then want to do the same with the Path class so we can use the assemble method. We’ll need to call this twice; the first time where we’re moving the file from, and where we’re moving it to. Your method should now look like:

/**
 * Moves files from one location to another.
 * You do not need to pass the addon name.
 * 
 * @param string $from     The location the files are currently in.
 * @param string $sitepath The folder in the sitepath to move the files to.
 * @param string $to       The location you want to move the files to.
 * 
 * @return void
 */
private function moveFiles(String $from, String $sitepath, String $to)
{
    File::copy(
        Path::assemble(

        ),
        Path::assemble(
            
        )
    );
}

The next thing we need to do is build the paths within the Path::assemblemethods. The first one we’ll tackle is the from path. Because this is always going to come from the addon, we can use addons_path($this->getAddonClassName()),. This returns the path of the addon via getting the name of the addon from its class. After we’ve done this, add $from after it and your method should look like the following:

/**
 * Moves files from one location to another.
 * You do not need to pass the addon name.
 * 
 * @param string $from     The path within the addon exc the addon name.
 * @param string $sitepath The folder in the sitepath to move the files to.
 * @param string $to       The path you want to move the files to.
 * 
 * @return void
 */
private function moveFiles(String $from, String $sitepath, String $to)
{
    File::copy(
        Path::assemble(
            addons_path($this->getAddonClassName()),
            $from
        ),
        Path::assemble(
            
        )
    );
}

Next, we need to add the path to where we want the file to end up. Now this is slightly different to the from path. Instead of grabbing the addon name, we need to pass through the name of the folder in the site path – hence the $sitepath parameter.

In the case of the fieldset, we’ll need to set this as settings. For the global, it will be content. To do this, we’ll set the first part of the second Path::assemble to be site_path($sitepath). Then we pass through the $to as we did with the $from.

The final thing we need to do is to set whether we want File::copy to overwrite any existing files with the same name. For the purpose of this demo, we’ll set it to be true, but you can decide what is best for your situation. You might (and probably should), for example, choose to throw an exception, but that is beyond the scope of this tutorial.

To test this works, we can set up the handle method to dump out the path that is being returned.

As a quick note, if you’re ever curious to make sure the command works, you can do something simple like:

public function handle()
{
    var_dump('Hello World');
}

And run companydetails:install to see it works. You should see Hello World appear in your terminal.

Once you’ve confirmed this works, the next step is to add the moveFiles function to the handle for each file. Your handle method should look like:

/**
 * The method fired when a user runs companydetails:install
 * 
 * @return void
 */
public function handle()
{
    // Copy the fieldset
    $this->moveFiles(
        'resources/fieldsets/company.yaml',
        'settings',
        'fieldsets/company.yaml'
    );

    // Copy the global
    $this->moveFiles(
        'resources/globals/company.yaml',
        'content',
        'globals/company.yaml'
    );
}

All that remains for you to do is run companydetails:install and you’ll see the files copied from the addon to the correct folders so they can be used by the site.

To recap, your file should look like:

<?php
/**
 * This command allows users to install the addon without manually needing
 * to move files to their required parts of the sites folder.
 * 
 * @package CompanyDetails
 * @author  Ben Furfie <hello@benfurfie.co.uk>
 * @license MIT https://opensource.org/licenses/MIT
 */
namespace Statamic\Addons\CompanyDetails\Commands;

use Statamic\Extend\Command;
use Statamic\API\File;
use Statamic\API\Path;

class InstallCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'companydetails:install';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Installs the CompanyDetails addon.';

    /**
     * The method fired when a user runs companydetails:install
     * 
     * @return void
     */
    public function handle()
    {
        // Copy the fieldset
        $this->moveFiles(
            'resources/fieldsets/company.yaml',
            'settings',
            'fieldsets/company.yaml'
        );

        // Copy the global
        $this->moveFiles(
            'resources/globals/company.yaml',
            'content',
            'globals/company.yaml'
        );
    }

    /**
     * Moves files from one location to another.
     * You do not need to pass the addon name.
     * 
     * @param string $from     The path within the addon exc the addon name.
     * @param string $sitepath The folder in the sitepath to move the files to.
     * @param string $to       The path you want to move the files to.
     * 
     * @return void
     */
    private function moveFiles(String $from, String $sitepath, String $to)
    {
        File::copy(
            Path::assemble(
                addons_path($this->getAddonClassName()),
                $from
            ),
            Path::assemble(
                site_path($sitepath),
                $to
            )
        );
    }
}

Homework

Right now, when you upload an image to the global, it will create a folder with the name company. However, this will be lowercase. If you’re like me, this will bother you.

To take what you’ve learnt and put it to practice, try to work out how to create a yaml file with a name defined for the company folder in assets and copy it over.

Let me know how you get on and if you have any questions!

Thanks

I have to thank Erin Dalzell for giving me the original inspiration for this approach in his History addon.