Why bother?

Theme’s on Magento usually are a manual install process (in fact I’ve never seen one myself that is well packaged outside of being a ZIP archive), and when working with a large number of hosts (like we do) it can be quite a tedious time sink to install the theme manually on every instance we spin up.

That’s when I had a thought… can’t we package our theme so we can distribute it to our clients instances using Composer, the same way we manage our suite of modules? According to Magento’s docs, the answer is yes!

There are a number of reasons why I thought this would be benificial, though I’m sure these are fairly obvious, why would you not want to keep you code in a VCS? Especially since most of the customers of the client I am working with all essentially use the same theme with minor customisations, having the code in a centralised repository means no more ‘cowboy coding’ where the files are edited directory on the remote host.

Creating the package

The theme that I am packaging has been developed by a colleague of mine, so the first step is pulling those theme files off the host that my colleague has been working on and creating a new Git repository on my local machine. All I did was stick the files in a tarball and download them to my machine using SCP (yes I know… should probably be using rsync by now, but I can remember the SCP syntax off the top of my head, whereas rsync I can’t)

Now that’s done, I need to structure the files so I can tell Composer to autoload them and get Magento to register them.

Using Luma as an example, I noticed that the files inside vendor/magento/theme-frontend-luma/ were all the theme files (theme.xml, registration.php etc). Essentially it is a single theme that does not include it’s parent, that is included by a seperate package.

This isn’t ideal, our theme that we distribute takes most of it’s templates, CSS & JS from its parent, and I didn’t fancy maintaining two seperate packages for what is essentially a single theme.

I needed a way to package all the themes (or ’theme layouts’) in a single package so that when I install said package, both the parent & child themes are included for me. This should actually be fairly straight forward, as the registration.php file is what registers the theme with Magento, and so long as I included this per theme I wanted to include, all should be OK.

The structure I went with is:

├── src
│   ├── layout01
│   ├── layout02
│   ├── layout03
│   ├── layout04
│   ├── layout05
│   ├── layout06
│   ├── layout07 <-- Our custom layout
│   ├── registration.php
├── composer.json
└── .gitignore

Since I have 7 different themes (think of them as theme layouts) within my single package, I didn’t fancy adding a registration.php file to all of them.

Luma’s registration.php is as follows:

use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(ComponentRegistrar::THEME, 'frontend/Magento/luma', __DIR__);

Since all my themes are in the directory src, I should be able to just iterate through all the directories inside my src directory and register them with Magento, without having to maintain seperate registration.php files for all of them.

use \Magento\Framework\Component\ComponentRegistrar;

// appends parent theme to the start of the array
$themeDir       = array_filter(glob(__DIR__ . '/*'), 'is_dir');
$parentTheme    = 'layout01';
$parentThemeDir = __DIR__ . '/' . $parentTheme;
if (($key = array_search($parentThemeDir, $themeDir)) !== false) {
    unset($themeDir[$key]);
}
array_unshift($themeDir, $parentThemeDir);

//register all layouts/themes
$package = 'mypackage';
foreach ($themeDir as $dir) {
    $pathParts = explode('/', $dir);
    $theme = end($pathParts);
    ComponentRegistrar::register(
        \Magento\Framework\Component\ComponentRegistrar::THEME,
        'frontend/JoeMet/' . $package . '_' . $theme,
        $dir
    );
}

All this does is ensure we register the parent theme (in this case ’layout01’) before we register any other themes. We then iterate through the list of themes and register them individually with Magento based on their directory name.

Now all I need to tell Composer to autoload is this single registration.php file.

{
  "name": "joemet/theme-frontend-mytheme",
  "type": "magento2-theme",
  "autoload": {
    "files": [
      "src/registration.php",
    ]
  }
}

I have omitted fields that aren’t relevant.

Great, now Magento should register all my themes once I tell Composer to require my new package. All that’s left to do is push my local Git repo to a remote and then publish the package to a repository that composer can download from. The client I am working for uses Github & a private Packagist repository, so that’s what we’ll use.

Everything is broken!

Everything seems to have worked, the Composer package was found & installed successfully, Magento registered the themes and shows me them as available in the admin panel… that is until I enable the theme on the frontend.

Now I’m getting an exception. Turns out I’d completely forgotten to include the modules that are shipped with the theme! If you’re familiar with Magento, you’re probably aware that most themes are shipped with a bundle of modules that provide functionality that is used by the theme.

At this point I was a bit stuck, as according to Abode’s docs if you’re packaging a theme, it’s type in composer.json needs to be set to magento2-theme and it’s name should follow the convention <vendor-name>/theme-<area>-<theme-name>. So I decided to have a look at a module we use a lot on our instances, ElasticSuite. Smile (the people who maintain ElasticSuite) seem to have set the type of their Composer package to magento2-component, interesting, this seems like something I can use. Confusingly when researching this new type, I found another page on the Adobde docs that does list magento2-component as a valid type, however it states:

The package formed of the files that must be located in root (.htaccess, etc). This includes dev/tests and setup as well for now.

I’m not entirely sure what this means, or if it’s enforced, but I’m going to try and use it anyway.

If you continue reading through this section of the docs, you’ll notice here that it’s stated you cannot mix extension types in a repository. However, for my use case that’s exactly what I want to do, these modules are only relevant in the context of this theme, and at no point will me or my colleagues be installing these independently of one another. As with quite a few things on Magento, their documentation isn’t necessarily gospel and so I’m going to give mixing extension types in a single repository a go.

Including the modules

Since I’m going to attempt to include the themes modules in my Composer package, I’ll need to restructure my package slightly.

The new structure I’m going with is:

├── themes
│   ├── layout01
│   ├── layout02
│   ├── layout03
│   ├── layout04
│   ├── layout05
│   ├── layout06
│   ├── layout07 <-- Our custom layout
│   ├── registration.php
├── modules
│   ├── module01
│   │   ├── registration.php
│   ├── module02
│   │   ├── registration.php
│   ├── module03
│   │   ├── registration.php
├── composer.json
└── .gitignore

You’ll notice here that I’ve not included a generic registration.php like I did with the themes, the reason being a) I just didn’t get round to it and b) it might making updating the modules easier when a new version is published by the developer.

Now I need to tell Composer to autoload the modules’ registration.php files, as well as mapping the namespaces to the corresponding file paths. My composer.json now looks like this:

{
  "name": "joemet/mytheme",
  "type": "magento2-component",
  "autoload": {
    "files": [
      "themes/registration.php",
      "modules/module01/registration.php",
      "modules/module02/registration.php",
      "modules/module03/registration.php",
    ],
    "psr-4": {
      "JoeMet\\ModuleOne\\": "modules/module01",
      "JoeMet\\ModuleTwo\\": "modules/module02",
      "JoeMet\\ModuleThree\\": "modules/module03",
    }
  }
}

Modules included, let’s install

Right - now I should be able to tell Magento to install my Composer package, and all of my theme layouts as well as the modules should be installed in one go.

composer require joemet/mytheme:dev-master

Composer was able to require the package successfully, time to check all the files exist.

ls -la vendor/joemet/mytheme

I can see both the theme/ and modules/ directories, so I think we’re good to go, let’s run Magento’s setup commands.

bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f en_GB en_US -j 8
bin/magento cache:flush

and finally let’s check the list of installed modules to ensure ours have been registered and included

bin/magento module:status | grep -i joemet

The modules have been installed and are enabled!

Conclusion