Managing Files With Composer

Submitted by Mile23 on Sun, 09/12/2021 - 12:37
Managing Files With Composer

Did you ever think, "Hey, I should be able to use Composer to move arbitrary files around in my codebase. I wonder why they didn't put that into Composer? Now I have to write a bash script..."?

Well guess what. You don't have to write that bash script.

You can configure Composer to manage files within your codebase. We'll use the Drupal Composer Scaffold plugin to do it. The obvious advantage here (other than not needing a bash script) is that the configuration is stored within composer.json, and thus, also your version control.

Another benefit is that when it comes time to build a Docker container image, we only need PHP and Composer as dependencies.

A note for our many non-Drupal PHP friends: This plugin is totally useful without Drupal. Need to move a file around in your codebase? Here's a solution.

You might wonder: Why do we need to do this?

One reason might be that whenever we run composer update, we have a chance of over-writing our special beloved bespoke .htaccess file with the default one from Drupal.

Since this might spell doom for our uptime, we'd like to make sure that running composer update or composer install always ensures that the correct .htaccess file is in the right place. The Drupal Composer Scaffold plugin performs this task.

The use-cases don't end with .htaccess, but we'll stick with that for now.

Note that we could also patch the .htaccess file using Drupal Composer Scaffold. We'll cover that in another article.

What We'll Do In This Article

Let's manage an .htaccess file using the Drupal Composer Scaffold plugin. This is a fairly common use case because everyone's Apache is configured just a little bit different, right?

We'll use the command line to create a new Drupal project to fiddle around with.

We'll modify the project's composer.json file to tell it to copy a file.

We'll use Composer to perform the file copy.

And finally we'll have ice cream and cake. Note: You must supply your own ice cream and cake.

HOWTO:

Let's start with a demo codebase. This is the recommended way to make a recommended Drupal codebase. It comes recommended.

You can type composer create-project drupal/recommended-project:@stable scaffold-demo into a terminal window:

% composer create-project drupal/recommended-project:@stable scaffold-demo
[ lots of stuff happens..... ]
% cd scaffold-demo 
% ls -al
total 360
drwxr-xr-x   8 paul  staff     256 Sep 11 17:11 .
drwxr-xr-x+ 84 paul  staff    2688 Sep 11 17:11 ..
-rw-r--r--   1 paul  staff     357 Sep 11 17:11 .editorconfig
-rw-r--r--   1 paul  staff    3858 Sep 11 17:11 .gitattributes
-rw-r--r--   1 paul  staff    3133 Sep  1 14:51 composer.json
-rw-r--r--   1 paul  staff  169991 Sep  1 14:51 composer.lock
drwxr-xr-x  20 paul  staff     640 Sep 11 17:11 vendor
drwxr-xr-x  20 paul  staff     640 Sep 11 17:11 web

Now we have a Drupal codebase, yay. Let's get to moving a file with the scaffold plugin.

First let's verify that the Drupal Composer Scaffold plugin exists:

% composer show drupal/core-composer-scaffold
name     : drupal/core-composer-scaffold
descrip. : A flexible Composer project scaffold builder.
keywords : drupal
versions : * 9.2.5
[ etc ]

If you're working on a project that doesn't have the scaffold plugin, you can add it like this:

% composer require drupal/core-composer-scaffold:@stable

Within our generic Drupal recommended project that we created above, we can see the existing scaffolding configuration in composer.json:

# composer.json
{
    ... more stuff here ....
    "extra": {
        "drupal-scaffold": {
            "locations": {
                "web-root": "web/"
            }
        }
    }
}

This tells the scaffold plugin that the location web-root is within the directory web/. This allows other parts of the configuration to refer to [web-root] and that will be replaced with web/. web-root is the only required location for the scaffolding plugin. Another common location is project-root, which is assumed to be the same as web-root if it's not provided. You can also add arbitrary locations as desired.

Next we should look at the magic scaffolding that the plugin does automatically for Drupal. Yes, MAGIC! The Drupal Composer Scaffold plugin will always add drupal/core's scaffolding without any configuration, if your project includes drupal/core. You can also add your own scaffolding to your own Composer packages, but that's another tutorial for another day.

Here's how drupal/core configures the scaffold plugin. We can see this in the file web/core/composer.json.

# web/core/composer.json
{
    "name": "drupal/core",
    ... more stuff here ...
    "extra": {
        "drupal-scaffold": {
            "file-mapping": {
                "[project-root]/.editorconfig": "assets/scaffold/files/editorconfig",
                "[project-root]/.gitattributes": "assets/scaffold/files/gitattributes",
                "[web-root]/.csslintrc": "assets/scaffold/files/csslintrc",
                "[web-root]/.eslintignore": "assets/scaffold/files/eslintignore",
                "[web-root]/.eslintrc.json": "assets/scaffold/files/eslintrc.json",
                "[web-root]/.ht.router.php": "assets/scaffold/files/ht.router.php",
                "[web-root]/.htaccess": "assets/scaffold/files/htaccess",
                "[web-root]/example.gitignore": "assets/scaffold/files/example.gitignore",
                "[web-root]/index.php": "assets/scaffold/files/index.php",
                "[web-root]/INSTALL.txt": "assets/scaffold/files/drupal.INSTALL.txt",
                "[web-root]/README.md": "assets/scaffold/files/drupal.README.md",
                "[web-root]/robots.txt": "assets/scaffold/files/robots.txt",
                "[web-root]/update.php": "assets/scaffold/files/update.php",
                "[web-root]/web.config": "assets/scaffold/files/web.config",
                "[web-root]/sites/README.txt": "assets/scaffold/files/sites.README.txt",
                "[web-root]/sites/development.services.yml": "assets/scaffold/files/development.services.yml",
                "[web-root]/sites/example.settings.local.php": "assets/scaffold/files/example.settings.local.php",
                "[web-root]/sites/example.sites.php": "assets/scaffold/files/example.sites.php",
                "[web-root]/sites/default/default.services.yml": "assets/scaffold/files/default.services.yml",
                "[web-root]/sites/default/default.settings.php": "assets/scaffold/files/default.settings.php",
                "[web-root]/modules/README.txt": "assets/scaffold/files/modules.README.txt",
                "[web-root]/profiles/README.txt": "assets/scaffold/files/profiles.README.txt",
                "[web-root]/themes/README.txt": "assets/scaffold/files/themes.README.txt"
            }
        }
    },
}

As an aside: This configuration is the secret sauce that allows for both drupal/recommended-project and drupal/legacy-project to co-exist in the same codebase, since it separates project root from web root.

As you can see, every file in drupal/core that isn't in the core/ directory is added by the scaffold plugin. (The core/ directory itself is configured using the composer/installers plugin. Check the installer-paths section.)

But the reason we bring it up here is because drupal/core demonstrates the scaffolding pattern for us: It has an assets/scaffold/ directory, from which the scaffold plugin will grab files and place them around the codebase.

We could, in turn, adopt the same pattern. We could have a similar directory in our project, and then have the scaffold plugin move files into place for us.

And that's what we're going to do. We'll call our directory scaffold/, and put our special bespoke .htaccess file in it.

% mkdir scaffold
% echo "# Note: Edit this in the scaffold directory" > scaffold/bespoke.htaccess 

Now we'll modify our drupal-scaffold configuration in composer.json:

# composer.json
{
    ... more stuff here ....
    "extra": {
        "drupal-scaffold": {
            "locations": {
                "web-root": "web/"
            },
            # Add this section:
            "file-mapping": {
                "[web-root]/.htaccess": "scaffold/bespoke.htaccess"
            }
        }
    }
}

This tells Drupal Composer Scaffold to 'map' .htaccess from scaffold/bespoke.htaccess. This is the simplest way to declare this mapping, which will just add or replace the .htaccess file from its source. (There are other ways to map files... We'll touch on that a little later.)

And now that it's configured, we can have scaffold perform the file copy for us, using the composer scaffold command:

% composer scaffold
Scaffolding files for drupal/recommended-project:
  - Copy [web-root]/.htaccess from scaffold/bespoke.htaccess

The composer scaffold command will check if any files need to be changed in the file system. That is, if you run composer scaffold again after it's finished, it won't report that it's doing anything, because the file is already up to date.

We can verify that it worked by reading the file:

% more web/.htaccess 
# Note: Edit this in the scaffold directory

So congratulations, you've moved a file in the codebase using Drupal Composer Scaffold.

But before we break out the cake and ice cream, we should consider some caveats for this technique.

Caveats

If you're committing to copying files around your file system, it can get a little unclear which files to modify, particularly for developers who didn't set it up.

In our case, when we update Drupal core, there might be a new security concern addressed by a change to core's .htaccess file, but our install process just obliterated it. So when we update core we have to take special care to notice that this has occurred.

And if, during our development process, we determine that we need to change our .htaccess file, we must keep in mind to change it in the scaffold/ directory, and then let Composer copy it over.

For other types of files, however, this downside might not exist at all, and so copy files with wild abandon! But be sure and tell all your devs, so they know not to edit scaffolded files in-place.

Next Steps

Clearly this technique can be applied to any number of files, not just .htaccess. As an example, for some sites, I use it for grabbing a favicon.ico file out of the theme directory and placing it in the web root.

But more importantly, if you read the documentation, you discover that this file-mapping configuration has a few extra bits to unlock...

You can specify file mapping modes for your scaffolded files. This allows you to select whether to replace, prepend, or append the scaffolded file onto an existing one. You can also tell the plugin not to replace existing files.

Fin.

Go now, and scaffold all the files!

You may now consume cake and ice cream.