Opinions: PHP Class Autoloading for Plugins

With the release of Kirby 2.4.0 I realized that to load the Toolkit and some other libraries, Kirby is now using Composer. One of the nice things about Composer is its default ClassLoader which is quite versatile and can be used to make my life as a developer much easier.

I quite often find myself writing fairly custom plugins for Kirby sites to keep the business logic modular and I’ve always held the opinion that PHP classes in Kirby Plugins should be loaded automatically without having to require them all. So, I figured since Kirby now includes the (for this purpose) relevant aspects of Composer that I could build something - which I did, and it works great for me.

Here’s what I did:
I add a ‘plugin-autoloader.php’ file to my webroot with the following content; This simply adds the ‘src’ directory of any plugin to a class loader namespace mapping for a namespace ‘Kirby\Plugins\PluginName’.

<?php
$dirs = array_filter(scandir($kirby->roots()->plugins()), function ($entry) { return $entry !== '.' && $entry !== '..'; });
$dirs = array_map(function ($entry) use ($kirby) { return ['name' => $entry, 'path' => $kirby->roots()->plugins() . DS . $entry]; }, $dirs);
$loader = new \Composer\Autoload\ClassLoader();

foreach ($dirs as $entry) {
    if (is_dir($entry['path']) && is_dir($entry['path'] . DS . 'src')) {
        $namespace = implode('', array_map(function ($word) { return ucfirst($word); }, preg_split('/[-_. +]/', 'json-api', $limit = -1, PREG_SPLIT_NO_EMPTY)));
        $loader->addPsr4('Kirby\\Plugins\\' . $namespace . '\\', $entry['path'] . DS . 'src');
    }
}

$loader->register();

Then I add a ‘site.php’ file

<?php
$kirby = kirby();
// configuration goes here

require(__DIR__ . DS . 'plugin-autoloader.php');

Now the questions I have for the community are these:

  • How do you usually deal with site-specific custom logic in general? Do you write plugins? Do you keep all the custom code in controllers? Other options?
  • What other ways of achieving a similar result are there?
  • Would it make sense to add something like this to core Kirby?
  • Do you have any suggestions on how to improve the class loader (besides adding some comments ;))

Kirby has a simple built-in autoloader, you can see it in use in the official plugins, e.g. in the Modules plugin.
It doesn’t use scandir on purpose (so you need to set an explicit classmap) to improve performance and security.

HA! Didn’t know that one. Thanks @lukasbestle ; the modules plugin looks like it could also help with some other things I’ve been working on. Two for one :slight_smile:

I’m currently working on the new v3 of my Uniform plugin. I wanted it to be installable with Composer, the Kirby CLI and in the “traditional” way (copied into site/plugins).

The Composer setup is straight forward. Add a composer.json, configure it to autoload the files from src/ and require an index.php which sets up some stuff with the Kirby extension registry and loads language files.

The Kirby CLI requires an additional package.json but then just downloads the repository and puts it into site/plugins. From here on it’s the “traditional” way.

For the plugin to be installed standalone I supply it’s own vendor directory which contains the dependencies and autoloader. This is just installed locally via Composer and committed to the repository. When the plugin directory is put to site/plugins/uniform Kirby will automatically load the uniform/uniform.php which in turn requires the uniform/vendor/autoload.php. From then on the plugin is autoloaded just like if it were installed via Composer in the first place.

This is quite a complicated setup but it’s compatible with the widest possible range of workflows. In fact Kirby and the Toolkit also supply their own vendor directories to autoload from so it’s not too unusual in this context. I hope Kirby will move more and more towards Composer and plugins can be installed “natively” via Composer some time in the future.

Very interesting!
Also: big fan of the uniform module :slight_smile: - will definitely take a closer look at the v3 code.

I see the omni-compatibility angle creates a bit of an overhead but it’s definitely compatible with almost anything. I quite like it. The one thing I (compulsively) dislike about that approach is that the vendor directory has to be checked in with the source just like Kirby does. Kinda defeats at least some of the purpose of composer.

I agree and I definitely prefer the installation via Composer. It’s only one additional line to the index.php and everything works. I wasn’t able to think of a better solution that also serves everyone but maybe someone will come up with one sometime. At least the additional vendor directory shouldn’t matter performance-wise.

I did the same for Auto Git. I wished there was a better way.

Now I’m actually curious to investigate what it would take to create a Kirby installation as a “from-scratch” composer project. Obviously the checked in vendors folder(s) would cause a problem with duplicates from package dependencies, but I could probably work around that for a prototype.

I think I’ve got myself an X-Mas side-project :slight_smile:

Take a look at the Composerkit first. Maybe that’s already what you are looking for :wink: I haven’t looked into it myself yet, but I definitely will.

For the record, the “composerkit” projet doesn’t deal with loading plugins. Mainly because every plugin has different types of support for Composer (none, published on Packagist or not, loads a main PHP file or uses Composer autoloading, etc.).

Note that the composer/installers plugin has some support for Kirby plugins, but I don’t know if that’s actually used in the wild. I think it mostly copies files around (to the site/plugins, site/tags and site/fields folders).

Correct but if a plugin supports installation via Composer it will work right away with all the benefits of sharing dependencies between the Kirby core and different plugins. The alternative of manually copying the plugin source to site/plugins should still work, right? If the plugins make use of the Kirby extension registry, copying files around for new tags or fields should not be necessary either.

“supports installation via Composer” can mean many things. At the very least it means that the plugin’s repository has a composer.json file with basic info (package name). From there on, you could have:

  • Published on GitHub or another VCS host only, or published on Packagist too?
  • Does the composer.json have dependencies?
  • Does the composer.json have PSR-4 class autoloading config?
  • Does the composer.json reference a file to be included by Composer (which should probably be different from the main pluginname.php, because at the point when Composer includes that file there is no guarantee that Kirby stuff will be loaded (classes should be autoloaded but the kirby() helper, I’m not sure… plus it might create subtle bugs to use kirby() in a plugin’s main script before your index.php or site.php have created the $kirby singleton instance or whatnot).
  • What are the steps for plugging your plugin in your Kirby instance, beyond that?

One strategy is using Composer to install the plugin to vendor/namespace/pluginname, then from site/plugins/load-plugins.php you could just include kirby()->roots()->index . '/vendor/namespace/pluginname/pluginname.php.

Another one is to do roughly the same thing but use a symlink instead of an include. Can be better if you have assets.

Or you can copy things over with mnsami/composer-custom-directory-installer.

Finally, a plugin could be “inert” by default and have a static method for registering itself.

It would be nice to have a convention, but I’m not sure which is best.

By “supports installation via Composer” I mean that you run composer require and it works. How everything is loaded and if there are dependencies should not matter. But you have a valid point with the kirby() helper of course. If I take the Starterkit and want to include the Composer autoloader I just have to make sure to include it after Kirby was initialized. But if Kirby is managed and autoloaded by Composer, too, that might be a problem. Laravel has the concept of service providers for this kind of thing so it can decide when to load a plugin and make sure everything important was already initialized. But that’s very different to Kirby and it’s plug and play mindset, not to mention the incompatibility with all existing plugins…

Edit: Instead of using the kirby() helper one could always use \Kirby::instance() which should work fine in an autoloaded environment.