,

Thaura Work: a plugin architecture for a PHP program

One of the code management techniques I tried with KRUpload was to break the application into a core app, and plugins. I don’t have stats about how that went, but it seemed to help.

Here’s a du of the extensions directory:

➜  extensions du -sh *
28K	BackupShare
24K	csv-preview
32K	download-url
16K	example
36K	file-editor
28K	FileNotes
20K	file-preview
44K	Fslint
16K	HelloWorld
404K	InternalTools ---- this is a huge db schema editor
28K	jpg-preview
20K	json-viewer
28K	m3u-preview
64K	MarkdownViewer
24K	mp3-preview
28K	png-preview
112K	Robots     ---- this should be in the Application.
28K	thumbnails
24K	TusUpload
24K	UploadProgress
24K	video-preview
20K	ZipDownload
Code language: JavaScript (javascript)

The core app is mostly in these directories:

➜  src du -sh Application Infrastructure Domain 
204K	Application
68K	Infrastructure
60K	Domain

While each plugin’s code size isn’t “small”, it’s smaller than the core application, and, perhaps more importantly, it’s separate from the core application.

It seems to help with LLM programming. The integration surfaces are better defined.

Separation also means it’s easy to throw away the entire plugin, and start over.

Architectural Patterns

The API for plugins is ad-hoc, but here are a few features.

Manifest

The plugin has a manifest.json file in the directory, and a program goes across the directories to gather them up, and register them inside a plugin registry.

The manifest is the main point of integration.

Directory Structure: Assets: frontend

Plugins are directories, and they contain assets that can be exposed to the “frontend”, which is the Javascript and HTML GUI. The GUI itself has a simple plugin architecture, as well.

This requires a build step, to copy the files into the public directories.

The alternative was to have a directory tree for these frontend assets, and then expose that to the web. I decided against that, because it would require managing two different programs in two different places.

I decided that I wanted all the related stuff in a single folder, so it could be ZIPped up, and distributed as an archive. (This is what WordPress does.)

Integration Points: Events, PubSub, Routes, Handlers, Hollywood Principle

Events and PubSub

The application uses the Publish and Subscribe pattern, also called events, and known as Actions and Filters in WordPress.

For example, the ‘file.uploaded’ event fires (is published) after a file uploads. The Thumbnails plugin subscribes to the file.uploaded event, and when it fires, a function in the plugin is called.

Routes and Handlers

The other thing plugins do is add REST API endpoints. The app is written in Slim4, so it just uses the framework’s extensions.

Dynamic vs. Lazy Loaded

Dynamic

The first extensions were defined by a file called backend.php. When the manifests were read, the backend.php script was loaded. REST API routes were defined, and event listeners registered. (More on this later.)

In PHP, this is inefficient, mainly because the entire PHP file gets read, compiled, then interpreted once. The result is cached, but each run of the program (which is each page request) requires at least the interpretation step.

Lazy Loaded

So, I decided to switch to a more difficult architecture centered around a collection of PHP features designed to make the language more efficient, by “lazy loading” code. The features center around using PHP’s OOP features, and class namespaces to avoid reading, compiling, and interpreting PHP files until they are needed.

The “need” is initially implied by the “use” keyword. Example:

    use Path\To\MyClass;

Now, at this moment, MyClass is NOT known to PHP. The code hasn’t been read, compiled, or interpreted. When the class is used, PHP will try to “autoload” the class. Example:

    new MyClass();

So that triggers an error, and there’s an error handler (written by the programmer, or generated by Composer) called an autoloader. The autoloader converts the full classname, “Path\To\MyClass”, into a filepath, like “./Path/To/MyClass.php”, and require()s it. That causes the file to be read, compiled, and interpreted.

The Lazy plugins also do another form of laziness: registering routes.

News routes for the REST API endpoints are declared in manifest.json. This avoids the slowdown of running PHP to establish endpoints.

All you need is the route, like: GET api/foo/bar
And the method: “\Application\ExternalExtensions\MyExtension\MyExtensionProvider::get”

Note on PubSub: Event listners are still registered in backend.php files. This should probably be moved to manifest.json, and lazy loaded, as well.

The Hollywood Principle: don’t call us, we’ll call you

All this registration of handlers is basically the Hollywood Principle, also known as Inversion of Control.

As the application runs, it calls registered handlers when they are needed.

In OOP programming, the practice is to use a class that implements an interface to organize the code. When the program runs, these methods are called by the application.

My plugin system doesn’t require implementing specific interfaces, but it’s still good practice to use consistent names. (I may require it in the future.)

JavaScript side integration

I didn’t plan out or execute the HTML and JS integration. The LLM came up with it, but the principles are similar:

  • When elements are available messages like onToolbarReady are fired.
  • Plugins register listeners for these events (PubSub pattern).
  • When the user interacts with some standard UI elements, like side panels, events get fired.
  • The URL uses a route after the # hash. When the route changes, an onRouteChange event fires.

The entire HTML document is the surface the plugin can modify. In practice, though, this usually means changing the content of specific elements, and not messing with most of the HTML.

Does it Work?

Yes. I thought the plugins would generally be small, but at one point, the LLM went off and created a skeletal Database Schema Editor. After many rounds of chat, it produced a half-baked, but functional, Schema and Table Editor for PostgreSQL.

Improvements?

The whole thing was improvised, but there are clearly key things that need to be locked in:

  • Manifests with routes and event listeners.
  • Interfaces for classes to implement, for route handlers, and for event handlers. (The challenge here is we don’t want big interfaces that require big implementations, because the LLMs don’t do well with large runs of code.)

Some other “nice to haves”:

  • Clean up the extensions directory so there is one directory of dynamic plugins, and one directory of lazy plugins.
  • Compilation of the lazy plugin manifest specs into PHP code that does all the registration. Compile time checks look for errors.
  • Compilation of the dynamic plugin manifests to speed up lookups, etc.
  • An end user config interface system, to abstract out policy, configs, etc.
  • Moving Extensions and Robots into the Domains namespace (which contains models (readonly classes) and interfaces).
  • Moving part of Extensions into Infrastructure.
admin
Author: admin

This is the server’s system administrator. This site is undergoing some changes.