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.