Skip to content

Commit

Permalink
Add civimix-schema with SchemaHelper and AutomaticUpgrader
Browse files Browse the repository at this point in the history
  • Loading branch information
totten committed Feb 27, 2024
1 parent 0ec7a4d commit 6247fbb
Show file tree
Hide file tree
Showing 6 changed files with 453 additions and 0 deletions.
28 changes: 28 additions & 0 deletions mixin/lib/civimix-schema/pathload.main.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
namespace CiviMix\Schema;

\pathload()->activatePackage('civimix-schema@5', __DIR__, [
'reloadable' => TRUE,
// The civimix-schema library specifically supports installation processes. From a
// bootstrap/service-availability POV, this is a rough environment which leads to
// the "Multi-Activation Issue" and "Multi-Download Issue". To adapt to them,
// civimix-schema follows "Reloadable Library" patterns.
// More information: https://github.com/totten/pathload-poc/blob/master/doc/issues.md
]);

// When reloading, we make newer instance of the Facade object.
$GLOBALS['CiviMixSchema5'] = require __DIR__ . '/src/CiviMixSchema5.php';

if (!interface_exists(__NAMESPACE__ . '\SchemaHelperInterface')) {
require __DIR__ . '/src/SchemaHelperInterface.php';
}

// \CiviMix\Schema\loadClass() is a facade. The facade should remain identical across versions.
if (!function_exists(__NAMESPACE__ . '\loadClass')) {

function loadClass(string $class) {
return $GLOBALS['CiviMixSchema5']->loadClass($class);
}

spl_autoload_register(__NAMESPACE__ . '\loadClass');
}
135 changes: 135 additions & 0 deletions mixin/lib/civimix-schema/src/AutomaticUpgrader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

namespace CiviMix\Schema;

/**
* The "AutomaticUpgrader" will create and destroy the SQL tables
* using XML schema files (`SchemaHelper`). It also calls-out to any custom
* upgrade code (eg `CRM_Myext_Upgrader`).
*
* To simplify backport considerations, `AutomaticUpgrader` does not have formal name.
* It is accessed via aliases like "CiviMix\Schema\*\AutomaticUpgrader".
*
* Target: CiviCRM v5.38+
*/
return new class() implements \CRM_Extension_Upgrader_Interface {

use \CRM_Extension_Upgrader_IdentityTrait {

init as initIdentity;

}

/**
* Optionally delegate to "CRM_Myext_Upgrader" or "Civi\Myext\Upgrader".
*
* @var \CRM_Extension_Upgrader_Interface|null
*/
private $customUpgrader;

public function init(array $params) {
$this->initIdentity($params);
if ($info = $this->getInfo()) {
if ($class = $this->getDelegateUpgraderClass($info)) {
$this->customUpgrader = new $class();
$this->customUpgrader->init($params);
if ($errors = $this->checkDelegateCompatibility($this->customUpgrader)) {
throw new \CRM_Core_Exception("AutomaticUpgrader is not compatible with $class:\n" . implode("\n", $errors));
}
}
}
}

public function notify(string $event, array $params = []) {
$info = $this->getInfo();
if (!$info) {
return;
}

if ($event === 'install') {
$GLOBALS['CiviMixSchema5']->getHelper($this->getExtensionKey())->install();
}

if ($this->customUpgrader) {
$this->customUpgrader->notify($event, $params);
}

if ($event === 'uninstall') {
$GLOBALS['CiviMixSchema5']->getHelper($this->getExtensionKey())->uninstall();
}
}

/**
* Civix-based extensions have a conventional name for their upgrader class ("CRM_Myext_Upgrader"
* or "Civi\Myext\Upgrader"). Figure out if this class exists.
*
* @param \CRM_Extension_Info $info
* @return string|null
* Ex: 'CRM_Myext_Upgrader' or 'Civi\Myext\Upgrader'
*/
public function getDelegateUpgraderClass(\CRM_Extension_Info $info): ?string {
$candidates = [];

if (!empty($info->civix['namespace'])) {
$namespace = $info->civix['namespace'];
$candidates[] = sprintf('%s_Upgrader', str_replace('/', '_', $namespace));
$candidates[] = sprintf('%s\\Upgrader', str_replace('/', '\\', $namespace));
}

foreach ($candidates as $candidate) {
if (class_exists($candidate)) {
return $candidate;
}
}

return NULL;
}

public function getInfo(): ?\CRM_Extension_Info {
try {
return \CRM_Extension_System::singleton()->getMapper()->keyToInfo($this->extensionName);
}
catch (\CRM_Extension_Exception_ParseException $e) {
\Civi::log()->error("Parse error in extension " . $this->extensionName . ": " . $e->getMessage());
return NULL;
}
}

/**
* @param \CRM_Extension_Upgrader_Interface $upgrader
* @return array
* List of error messages.
*/
public function checkDelegateCompatibility($upgrader): array {
$class = get_class($upgrader);

$errors = [];

if (!($upgrader instanceof \CRM_Extension_Upgrader_Base)) {
$errors[] = "$class is not based on CRM_Extension_Upgrader_Base.";
return $errors;
}

// In the future, we will probably modify AutomaticUpgrader to build its own
// sequence of revisions (based on other sources of data). AutomaticUpgrader
// is only regarded as compatible with classes that strictly follow the standard revision-model.
$methodNames = [
'appendTask',
'onUpgrade',
'getRevisions',
'getCurrentRevision',
'setCurrentRevision',
'enqueuePendingRevisions',
'hasPendingRevisions',
];
foreach ($methodNames as $methodName) {
$method = new \ReflectionMethod($upgrader, $methodName);
if ($method->getDeclaringClass()->getName() !== 'CRM_Extension_Upgrader_Base') {
$errors[] = "To ensure future interoperability, AutomaticUpgrader only supports {$class}::{$methodName}() if it's inherited from CRM_Extension_Upgrader_Base";
}
}

return $errors;
}

};
46 changes: 46 additions & 0 deletions mixin/lib/civimix-schema/src/CiviMixSchema5.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
namespace CiviMix\Schema;

/**
* This object is known as $GLOBALS['CiviMixSchema5']. It is a reloadable service-object.
* (It may be reloaded if you enable a new extension that includes an upgraded copy.)
*/
return new class() {

/**
* @var string
* Regular expression. Note the 2 groupings. $m[1] identifies a per-extension namespace. $m[2] identifies the actual class.
*/
private $regex = ';^CiviMix\\\Schema\\\(\w+)\\\(AutomaticUpgrader)$;';

/**
* If someone requests a class like:
*
* CiviMix\Schema\MyExt\AutomaticUpgrader
*
* then load the latest version of:
*
* civimix-schema/src/Helper.php
*/
public function loadClass(string $class) {
if (preg_match($this->regex, $class, $m)) {
$absPath = __DIR__ . DIRECTORY_SEPARATOR . $m[2] . '.php';
class_alias(get_class(require $absPath), $class);
}
}

/**
* @param string $extensionKey
* Ex: 'org.civicrm.flexmailer'
* @return \CiviMix\Schema\SchemaHelperInterface
*/
public function getHelper(string $extensionKey) {
$store = &\Civi::$statics['CiviMixSchema5-helpers'];
if (!isset($store[$extensionKey])) {
$class = get_class(require __DIR__ . '/SchemaHelper.php');
$store[$extensionKey] = new $class($extensionKey);
}
return $store[$extensionKey];
}

};
Loading

0 comments on commit 6247fbb

Please sign in to comment.