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`