civix#175 - Add support for mixins. Use MixinScanner/MixinLoader and boot-cache.
authorTim Otten <totten@civicrm.org>
Tue, 14 Jul 2020 07:29:07 +0000 (00:29 -0700)
committerTim Otten <totten@civicrm.org>
Wed, 1 Dec 2021 03:36:08 +0000 (19:36 -0800)
commit92ee7b1934727848a2c5e2debf3a41b553f81d2b
treedc869cf2e019a37b60e448ee607a48bc1f697d53
parent8af4ca4f8229b60bb9dce410748001a44939ac6c
civix#175 - Add support for mixins. Use MixinScanner/MixinLoader and boot-cache.

Overview
--------

(NOTE: For this description, I reference the term "API" in the general sense of a programmatic interface -- such as
a hook or file-naming convention. It is not specifically about CRUD/DB APIs.)

The `civix` code-generator provides support for additional coding-conventions -- ones which are more amenable to
code-generation.  For example, it autoloads files from `xml/Menu/*.xml` and `**/*.mgd.php`.  The technique for
implementing this traditionally relies on generating a lot of boilerplate.

This patch introduces a new construct ("mixin") which allows boilerplate to be maintained more easily.  A mixin
inspects an extension programmatically, registering new hooks as needed.  A mixin may start out as a file in `civix`
(or even as a bespoke file in some module) - and then be migrated into `civicrm-core`. Each mixin has a name and
version, which means that (at runtime) it will only load the mixin once (ie the best-available version).

See: https://github.com/totten/civix/issues/175

Before
------

The civix templates generate a few files, such as `mymod.php` and `mymod.civix.php`.
A typical example looks like this:

```php
// mymod.php - Implement hook_civicrm_xmlMenu
require_once 'mymod.civix.php';
function mymod_civicrm_xmlMenu(&$all, $the, $params) {
  _mymod_civix_civicrm_xmlMenu($all, $the, $params);
}
```

and

```php
// mymod.civix.php - Implement hook_civicrm_xmlMenu
function _mymod_civix_civicrm_xmlMenu(&$all, $the, $params) {
  foreach (_mosaico_civix_glob(__DIR__ . '/xml/Menu/*.xml') as $file) {
    $files[] = $file;
  }
}
```

These two files are managed differently: `mymod.php` is owned by the developer, and they may add/remove/manage the
hooks in this file.  `mymod.civix.php` is owned by `civix` and must be autogenerated.

This structure allows `civix` (and any `civix`-based extension) to take advantage of new coding-convention
immediately. However, it comes with a few pain-points:

* If you want to write a patch for `_mymod_civix_civicrm_xmlMenu`, the dev-test-loop requires several steps.
* If `civix` needs to add a new `hook_civicrm_foo`, then the author must manually create the stub
  function in `mymod.php`. `civix` has documentation (`UPGRADE.md`) which keeps a long list of stubs that must
  be manually added.
* If `civix` has an update for `_mymod_civix_civicrm_xmlMenu`, then the author must regenerate `mymod.civix.php`.
* If `mymod_civix_xmlMenu` needs a change, then the author must apply it manually.
* If `civix`'s spin on `hook_civicrm_xmlMenu` becomes widespread, then the `xmlMenu` boilerplate is duplicated
  across many extensions.

After
-----

An extension may enable a mixin in `info.xml`, eg:

```xml
<mixins>
  <mixin>civix-register-files@2.0</mixin>
</mixins>
```

Civi will look for a file `mixin/civicrm-register-files@2.0.0.mixin.php` (either in the extension or core). The file follows this pattern:

```php
return function(\CRM_Extension_MixInfo $mixInfo, \CRM_Extension_BootCache $bootCache) {
  // echo "This is " . $mixInfo->longName . "!\n";
  \Civi::dispatcher()->addListener("hook_civicrm_xmlMenu", function($e) use ($mixInfo) {
    ...
  });
}
```

The mixin file is a plain PHP file that can be debugged/copied/edited verbatim, and it can register for hooks on its
own.  The code is no longer a "template", and it doesn't need to be interwoven between `mymod.php` and
`mymod.civix.php`.

It is expected that a system may have multiple copies of a mixin.  It will choose the newest compatible copy.
Hypothetically, if there were a security update or internal API change, core might ship a newer version to supplant the
old copy in any extensions.

Technical Details
-----------------

Mixins may define internal classes/interfaces/functions. However, each major-version
must have a distinct prefix (e.g. `\V2\Mymixin\FooInterface`). Minor-versions may be
provide incremental revisions over the same symbol (but it's imperative for newer
increments to provide the backward-compatibility).

MixinScanner - Make it easier to instantiate and pay with instances

Ex: cv ev '$o=new CRM_Extension_MixinScanner(); var_export($o->createLoader());'

MixinScanner - Enable scanning of `[civicrm.root]/mixin`
CRM/Extension/BootCache.php [new file with mode: 0644]
CRM/Extension/ClassLoader.php
CRM/Extension/Info.php
CRM/Extension/MixInfo.php [new file with mode: 0644]
CRM/Extension/MixinLoader.php [new file with mode: 0644]
CRM/Extension/MixinScanner.php [new file with mode: 0644]