mirror of
https://codeberg.org/artfulrobot/contactcats.git
synced 2025-06-26 01:38:04 +02:00
checkin (broken)
This commit is contained in:
parent
a01abba787
commit
06fec5daf7
21 changed files with 2025 additions and 376 deletions
28
mixin/lib/civimix-schema@5.80.2/pathload.main.php
Normal file
28
mixin/lib/civimix-schema@5.80.2/pathload.main.php
Normal file
|
@ -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['CiviMixSchema'] = require __DIR__ . '/src/CiviMixSchema.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['CiviMixSchema']->loadClass($class);
|
||||
}
|
||||
|
||||
spl_autoload_register(__NAMESPACE__ . '\loadClass');
|
||||
}
|
181
mixin/lib/civimix-schema@5.80.2/src/AutomaticUpgrader.php
Normal file
181
mixin/lib/civimix-schema@5.80.2/src/AutomaticUpgrader.php
Normal file
|
@ -0,0 +1,181 @@
|
|||
<?php
|
||||
|
||||
namespace CiviMix\Schema;
|
||||
|
||||
use Civi\Test\Invasive;
|
||||
|
||||
/**
|
||||
* The "AutomaticUpgrader" will create and destroy the SQL tables
|
||||
* using 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['CiviMixSchema']->getHelper($this->getExtensionKey())->install();
|
||||
}
|
||||
|
||||
if ($this->customUpgrader) {
|
||||
$result = $this->customUpgrader->notify($event, $params);
|
||||
// for upgrade checks, we need to pass check results up to the caller
|
||||
// (for now - could definitely be more elegant!)
|
||||
if ($event === 'upgrade') {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if ($event === 'uninstall') {
|
||||
$GLOBALS['CiviMixSchema']->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;
|
||||
}
|
||||
|
||||
public function __set($property, $value) {
|
||||
switch ($property) {
|
||||
// _queueAdapter() needs these properties.
|
||||
case 'ctx':
|
||||
case 'queue':
|
||||
if (!$this->customUpgrader) {
|
||||
throw new \RuntimeException("AutomaticUpgrader($this->extensionName): Cannot assign delegated property: $property (No custom-upgrader found)");
|
||||
}
|
||||
// "Invasive": unlike QueueTrait, we are not in the same class as the recipient. And we can't replace previously-published QueueTraits.
|
||||
Invasive::set([$this->customUpgrader, $property], $value);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new \RuntimeException("AutomaticUpgrader($this->extensionName): Cannot assign unknown property: $property");
|
||||
}
|
||||
|
||||
public function __get($property) {
|
||||
switch ($property) {
|
||||
// _queueAdapter() needs these properties.
|
||||
case 'ctx':
|
||||
case 'queue':
|
||||
if (!$this->customUpgrader) {
|
||||
throw new \RuntimeException("AutomaticUpgrader($this->extensionName): Cannot read delegated property: $property (No custom-upgrader found)");
|
||||
}
|
||||
// "Invasive": Unlike QueueTrait, we are not in the same class as the recipient. And we can't replace previously-published QueueTraits.
|
||||
return Invasive::get([$this->customUpgrader, $property]);
|
||||
}
|
||||
throw new \RuntimeException("AutomaticUpgrader($this->extensionName): Cannot read unknown property: $property");
|
||||
}
|
||||
|
||||
public function __call($name, $arguments) {
|
||||
if ($this->customUpgrader) {
|
||||
return call_user_func_array([$this->customUpgrader, $name], $arguments);
|
||||
}
|
||||
else {
|
||||
throw new \RuntimeException("AutomaticUpgrader($this->extensionName): Cannot delegate method $name (No custom-upgrader found)");
|
||||
}
|
||||
}
|
||||
|
||||
};
|
46
mixin/lib/civimix-schema@5.80.2/src/CiviMixSchema.php
Normal file
46
mixin/lib/civimix-schema@5.80.2/src/CiviMixSchema.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
namespace CiviMix\Schema;
|
||||
|
||||
/**
|
||||
* This object is known as $GLOBALS['CiviMixSchema']. 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|DAO)$;';
|
||||
|
||||
/**
|
||||
* 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['CiviMixSchema-helpers'];
|
||||
if (!isset($store[$extensionKey])) {
|
||||
$class = get_class(require __DIR__ . '/SchemaHelper.php');
|
||||
$store[$extensionKey] = new $class($extensionKey);
|
||||
}
|
||||
return $store[$extensionKey];
|
||||
}
|
||||
|
||||
};
|
350
mixin/lib/civimix-schema@5.80.2/src/DAO.php
Normal file
350
mixin/lib/civimix-schema@5.80.2/src/DAO.php
Normal file
|
@ -0,0 +1,350 @@
|
|||
<?php
|
||||
|
||||
namespace CiviMix\Schema;
|
||||
|
||||
/**
|
||||
* To simplify backport considerations, `DAO` does not have formal name.
|
||||
* It is accessed via aliases like "CiviMix\Schema\*\DAO".
|
||||
*
|
||||
* Target: TBD (5.38+? 5.51+)
|
||||
*/
|
||||
return new class() extends \CRM_Core_DAO {
|
||||
|
||||
public function __construct() {
|
||||
if (strpos(static::class, '@') !== FALSE) {
|
||||
// Template instance. Fake news!
|
||||
return;
|
||||
}
|
||||
parent::__construct();
|
||||
// Historically a generated DAO would have one class variable per field.
|
||||
// To prevent undefined property warnings, this dynamic DAO mimics that by
|
||||
// initializing the object with a property for each field.
|
||||
foreach (static::getEntityDefinition()['getFields']() as $name => $field) {
|
||||
$this->$name = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function keys(): array {
|
||||
$keys = [];
|
||||
foreach (static::getEntityDefinition()['getFields']() as $name => $field) {
|
||||
if (!empty($field['primary_key'])) {
|
||||
$keys[] = $name;
|
||||
}
|
||||
}
|
||||
return $keys;
|
||||
}
|
||||
|
||||
public static function getEntityTitle($plural = FALSE) {
|
||||
$info = static::getEntityInfo();
|
||||
return ($plural && isset($info['title_plural'])) ? $info['title_plural'] : $info['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getEntityPaths(): array {
|
||||
$definition = static::getEntityDefinition();
|
||||
if (isset($definition['getPaths'])) {
|
||||
return $definition['getPaths']();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function getLabelField(): ?string {
|
||||
return static::getEntityInfo()['label_field'] ?? NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getEntityDescription(): ?string {
|
||||
return static::getEntityInfo()['description'] ?? NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getTableName() {
|
||||
return static::getEntityDefinition()['table'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getLog(): bool {
|
||||
return static::getEntityInfo()['log'] ?? FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getEntityIcon(string $entityName, ?int $entityId = NULL): ?string {
|
||||
return static::getEntityInfo()['icon'] ?? NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected static function getTableAddVersion(): string {
|
||||
return static::getEntityInfo()['add'] ?? '1.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getExtensionName(): ?string {
|
||||
return static::getEntityDefinition()['module'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function &fields() {
|
||||
$fields = [];
|
||||
foreach (static::getSchemaFields() as $field) {
|
||||
$key = $field['uniqueName'] ?? $field['name'];
|
||||
unset($field['uniqueName']);
|
||||
$fields[$key] = $field;
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
|
||||
private static function getSchemaFields(): array {
|
||||
if (!isset(\Civi::$statics[static::class]['fields'])) {
|
||||
\Civi::$statics[static::class]['fields'] = static::loadSchemaFields();
|
||||
}
|
||||
return \Civi::$statics[static::class]['fields'];
|
||||
}
|
||||
|
||||
private static function loadSchemaFields(): array {
|
||||
$fields = [];
|
||||
$entityDef = static::getEntityDefinition();
|
||||
$baoName = \CRM_Core_DAO_AllCoreTables::getBAOClassName(static::class);
|
||||
|
||||
foreach ($entityDef['getFields']() as $fieldName => $fieldSpec) {
|
||||
$field = [
|
||||
'name' => $fieldName,
|
||||
'type' => !empty($fieldSpec['data_type']) ? \CRM_Utils_Type::getValidTypes()[$fieldSpec['data_type']] : static::getCrmTypeFromSqlType($fieldSpec['sql_type']),
|
||||
'title' => $fieldSpec['title'],
|
||||
'description' => $fieldSpec['description'] ?? NULL,
|
||||
];
|
||||
if (!empty($fieldSpec['required'])) {
|
||||
$field['required'] = TRUE;
|
||||
}
|
||||
if (strpos($fieldSpec['sql_type'], 'decimal(') === 0) {
|
||||
$precision = self::getFieldLength($fieldSpec['sql_type']);
|
||||
$field['precision'] = array_map('intval', explode(',', $precision));
|
||||
}
|
||||
foreach (['maxlength', 'size', 'rows', 'cols'] as $attr) {
|
||||
if (isset($fieldSpec['input_attrs'][$attr])) {
|
||||
$field[$attr] = $fieldSpec['input_attrs'][$attr];
|
||||
unset($fieldSpec['input_attrs'][$attr]);
|
||||
}
|
||||
}
|
||||
if (strpos($fieldSpec['sql_type'], 'char(') !== FALSE) {
|
||||
$length = self::getFieldLength($fieldSpec['sql_type']);
|
||||
if (!isset($field['size'])) {
|
||||
$field['size'] = constant(static::getDefaultSize($length));
|
||||
}
|
||||
if (!isset($field['maxlength'])) {
|
||||
$field['maxlength'] = $length;
|
||||
}
|
||||
}
|
||||
$usage = $fieldSpec['usage'] ?? [];
|
||||
$field['usage'] = [
|
||||
'import' => in_array('import', $usage),
|
||||
'export' => in_array('export', $usage),
|
||||
'duplicate_matching' => in_array('duplicate_matching', $usage),
|
||||
'token' => in_array('token', $usage),
|
||||
];
|
||||
if ($field['usage']['import']) {
|
||||
$field['import'] = TRUE;
|
||||
}
|
||||
$field['where'] = $entityDef['table'] . '.' . $field['name'];
|
||||
if ($field['usage']['export'] || (!$field['usage']['export'] && $field['usage']['import'])) {
|
||||
$field['export'] = $field['usage']['export'];
|
||||
}
|
||||
if (!empty($fieldSpec['contact_type'])) {
|
||||
$field['contactType'] = $fieldSpec['contact_type'];
|
||||
}
|
||||
if (!empty($fieldSpec['permission'])) {
|
||||
$field['permission'] = $fieldSpec['permission'];
|
||||
}
|
||||
if (array_key_exists('default', $fieldSpec)) {
|
||||
$field['default'] = isset($fieldSpec['default']) ? (string) $fieldSpec['default'] : NULL;
|
||||
if (is_bool($fieldSpec['default'])) {
|
||||
$field['default'] = $fieldSpec['default'] ? '1' : '0';
|
||||
}
|
||||
}
|
||||
$field['table_name'] = $entityDef['table'];
|
||||
$field['entity'] = $entityDef['name'];
|
||||
$field['bao'] = $baoName;
|
||||
$field['localizable'] = intval($fieldSpec['localizable'] ?? 0);
|
||||
if (!empty($fieldSpec['localize_context'])) {
|
||||
$field['localize_context'] = (string) $fieldSpec['localize_context'];
|
||||
}
|
||||
if (!empty($fieldSpec['entity_reference'])) {
|
||||
if (!empty($fieldSpec['entity_reference']['entity'])) {
|
||||
$field['FKClassName'] = static::getDAONameForEntity($fieldSpec['entity_reference']['entity']);
|
||||
}
|
||||
if (!empty($fieldSpec['entity_reference']['dynamic_entity'])) {
|
||||
$field['DFKEntityColumn'] = $fieldSpec['entity_reference']['dynamic_entity'];
|
||||
}
|
||||
$field['FKColumnName'] = $fieldSpec['entity_reference']['key'] ?? 'id';
|
||||
}
|
||||
if (!empty($fieldSpec['component'])) {
|
||||
$field['component'] = $fieldSpec['component'];
|
||||
}
|
||||
if (!empty($fieldSpec['serialize'])) {
|
||||
$field['serialize'] = $fieldSpec['serialize'];
|
||||
}
|
||||
if (!empty($fieldSpec['unique_name'])) {
|
||||
$field['uniqueName'] = $fieldSpec['unique_name'];
|
||||
}
|
||||
if (!empty($fieldSpec['unique_title'])) {
|
||||
$field['unique_title'] = $fieldSpec['unique_title'];
|
||||
}
|
||||
if (!empty($fieldSpec['deprecated'])) {
|
||||
$field['deprecated'] = TRUE;
|
||||
}
|
||||
if (!empty($fieldSpec['input_attrs'])) {
|
||||
$field['html'] = \CRM_Utils_Array::rekey($fieldSpec['input_attrs'], function($str) {
|
||||
return \CRM_Utils_String::convertStringToCamel($str, FALSE);
|
||||
});
|
||||
}
|
||||
if (!empty($fieldSpec['input_type'])) {
|
||||
$field['html']['type'] = $fieldSpec['input_type'];
|
||||
}
|
||||
if (!empty($fieldSpec['pseudoconstant'])) {
|
||||
$field['pseudoconstant'] = \CRM_Utils_Array::rekey($fieldSpec['pseudoconstant'], function($str) {
|
||||
return \CRM_Utils_String::convertStringToCamel($str, FALSE);
|
||||
});
|
||||
if (!isset($field['pseudoconstant']['optionEditPath']) && !empty($field['pseudoconstant']['optionGroupName'])) {
|
||||
$field['pseudoconstant']['optionEditPath'] = 'civicrm/admin/options/' . $field['pseudoconstant']['optionGroupName'];
|
||||
}
|
||||
}
|
||||
if (!empty($fieldSpec['primary_key']) || !empty($fieldSpec['readonly'])) {
|
||||
$field['readonly'] = TRUE;
|
||||
}
|
||||
$field['add'] = $fieldSpec['add'] ?? NULL;
|
||||
$fields[$fieldName] = $field;
|
||||
}
|
||||
\CRM_Core_DAO_AllCoreTables::invoke(static::class, 'fields_callback', $fields);
|
||||
return $fields;
|
||||
}
|
||||
|
||||
private static function getFieldLength($sqlType): ?string {
|
||||
$open = strpos($sqlType, '(');
|
||||
if ($open) {
|
||||
return substr($sqlType, $open + 1, -1);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function indices(bool $localize = TRUE): array {
|
||||
$definition = static::getEntityDefinition();
|
||||
$indices = [];
|
||||
if (isset($definition['getIndices'])) {
|
||||
$fields = $definition['getFields']();
|
||||
foreach ($definition['getIndices']() as $name => $info) {
|
||||
$index = [
|
||||
'name' => $name,
|
||||
'field' => [],
|
||||
'localizable' => FALSE,
|
||||
];
|
||||
foreach ($info['fields'] as $fieldName => $length) {
|
||||
if (!empty($fields[$fieldName]['localizable'])) {
|
||||
$index['localizable'] = TRUE;
|
||||
}
|
||||
if (is_int($length)) {
|
||||
$fieldName .= "($length)";
|
||||
}
|
||||
$index['field'][] = $fieldName;
|
||||
}
|
||||
if (!empty($info['unique'])) {
|
||||
$index['unique'] = TRUE;
|
||||
}
|
||||
$index['sig'] = ($definition['table']) . '::' . intval($info['unique'] ?? 0) . '::' . implode('::', $index['field']);
|
||||
$indices[$name] = $index;
|
||||
}
|
||||
}
|
||||
return ($localize && $indices) ? \CRM_Core_DAO_AllCoreTables::multilingualize(static::class, $indices) : $indices;
|
||||
}
|
||||
|
||||
public static function getEntityDefinition(): array {
|
||||
if (!isset(\Civi::$statics[static::class]['definition'])) {
|
||||
$class = new \ReflectionClass(static::class);
|
||||
$file = substr(basename($class->getFileName()), 0, -4) . '.entityType.php';
|
||||
$folder = dirname($class->getFileName(), 4) . '/schema/';
|
||||
$path = $folder . $file;
|
||||
\Civi::$statics[static::class]['definition'] = include $path;
|
||||
}
|
||||
return \Civi::$statics[static::class]['definition'];
|
||||
}
|
||||
|
||||
private static function getEntityInfo(): array {
|
||||
return static::getEntityDefinition()['getInfo']();
|
||||
}
|
||||
|
||||
private static function getDefaultSize($length) {
|
||||
// Infer from <length> tag if <size> was not explicitly set or was invalid
|
||||
// This map is slightly different from CRM_Core_Form_Renderer::$_sizeMapper
|
||||
// Because we usually want fields to render as smaller than their maxlength
|
||||
$sizes = [
|
||||
2 => 'TWO',
|
||||
4 => 'FOUR',
|
||||
6 => 'SIX',
|
||||
8 => 'EIGHT',
|
||||
16 => 'TWELVE',
|
||||
32 => 'MEDIUM',
|
||||
64 => 'BIG',
|
||||
];
|
||||
foreach ($sizes as $size => $name) {
|
||||
if ($length <= $size) {
|
||||
return "CRM_Utils_Type::$name";
|
||||
}
|
||||
}
|
||||
return 'CRM_Utils_Type::HUGE';
|
||||
}
|
||||
|
||||
private static function getCrmTypeFromSqlType(string $sqlType): int {
|
||||
[$type] = explode('(', $sqlType);
|
||||
switch ($type) {
|
||||
case 'varchar':
|
||||
case 'char':
|
||||
return \CRM_Utils_Type::T_STRING;
|
||||
|
||||
case 'datetime':
|
||||
return \CRM_Utils_Type::T_DATE + \CRM_Utils_Type::T_TIME;
|
||||
|
||||
case 'decimal':
|
||||
return \CRM_Utils_Type::T_MONEY;
|
||||
|
||||
case 'double':
|
||||
return \CRM_Utils_Type::T_FLOAT;
|
||||
|
||||
case 'int unsigned':
|
||||
case 'tinyint':
|
||||
return \CRM_Utils_Type::T_INT;
|
||||
|
||||
default:
|
||||
return constant('CRM_Utils_Type::T_' . strtoupper($type));
|
||||
}
|
||||
}
|
||||
|
||||
private static function getDAONameForEntity($entity) {
|
||||
if (is_callable(['CRM_Core_DAO_AllCoreTables', 'getDAONameForEntity'])) {
|
||||
return \CRM_Core_DAO_AllCoreTables::getDAONameForEntity($entity);
|
||||
}
|
||||
else {
|
||||
return \CRM_Core_DAO_AllCoreTables::getFullName($entity);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
87
mixin/lib/civimix-schema@5.80.2/src/SchemaHelper.php
Normal file
87
mixin/lib/civimix-schema@5.80.2/src/SchemaHelper.php
Normal file
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
namespace CiviMix\Schema;
|
||||
|
||||
/**
|
||||
* The "SchemaHelper" class provides helper methods for an extension to manage its schema.
|
||||
*
|
||||
* Target: CiviCRM v5.38+
|
||||
*/
|
||||
return new class() implements SchemaHelperInterface {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* Ex: 'org.civicrm.flexmailer'
|
||||
*/
|
||||
private $key;
|
||||
|
||||
private $sqlGenerator;
|
||||
|
||||
public function __construct(?string $key = NULL) {
|
||||
$this->key = $key;
|
||||
}
|
||||
|
||||
public function install(): void {
|
||||
$this->runSqls([$this->generateInstallSql()]);
|
||||
}
|
||||
|
||||
public function uninstall(): void {
|
||||
$this->runSqls([$this->generateUninstallSql()]);
|
||||
}
|
||||
|
||||
public function generateInstallSql(): ?string {
|
||||
return $this->getSqlGenerator()->getCreateTablesSql();
|
||||
}
|
||||
|
||||
public function generateUninstallSql(): string {
|
||||
return $this->getSqlGenerator()->getDropTablesSql();
|
||||
}
|
||||
|
||||
public function hasSchema(): bool {
|
||||
return file_exists($this->getExtensionDir() . '/schema');
|
||||
}
|
||||
|
||||
public function arrayToSql(array $entityDefn): string {
|
||||
$generator = $this->getSqlGenerator();
|
||||
return $generator->generateCreateTableWithConstraintSql($entityDefn);
|
||||
}
|
||||
|
||||
// FIXME: You can add more utility methods here
|
||||
|
||||
// public function addTables(array $names): void {
|
||||
// throw new \RuntimeException("TODO: Install a single tables");
|
||||
// }
|
||||
//
|
||||
// public function addColumn(string $table, string $column): void {
|
||||
// throw new \RuntimeException("TODO: Install a single tables");
|
||||
// }
|
||||
|
||||
/**
|
||||
* @param array $sqls
|
||||
* List of SQL scripts.
|
||||
*/
|
||||
private function runSqls(array $sqls): void {
|
||||
foreach ($sqls as $sql) {
|
||||
\CRM_Utils_File::runSqlQuery(CIVICRM_DSN, $sql);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getExtensionDir(): string {
|
||||
if ($this->key === 'civicrm') {
|
||||
$r = new \ReflectionClass('CRM_Core_ClassLoader');
|
||||
return dirname($r->getFileName(), 3);
|
||||
}
|
||||
$system = \CRM_Extension_System::singleton();
|
||||
return $system->getMapper()->keyToBasePath($this->key);
|
||||
}
|
||||
|
||||
private function getSqlGenerator() {
|
||||
if ($this->sqlGenerator === NULL) {
|
||||
$gen = require __DIR__ . '/SqlGenerator.php';
|
||||
$this->sqlGenerator = $gen::createFromFolder($this->key, $this->getExtensionDir() . '/schema', $this->key === 'civicrm');
|
||||
}
|
||||
return $this->sqlGenerator;
|
||||
}
|
||||
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace CiviMix\Schema;
|
||||
|
||||
/**
|
||||
* The SchemaHelperInterface provides utility methods for managing the schema
|
||||
* in an extension (e.g. installing or uninstalling all SQL tables).
|
||||
*
|
||||
* The interface is implemented by the reloadable library (civimix-schema@5). To ensure
|
||||
* newer revisions of the library can be loaded, the implementation is an anonymous-class,
|
||||
* and the interface uses soft type-hints.
|
||||
*
|
||||
* @method bool hasSchema()
|
||||
*
|
||||
* @method void install()
|
||||
* @method void uninstall()
|
||||
*
|
||||
* @method string generateInstallSql()
|
||||
* @method string generateUninstallSql()
|
||||
*
|
||||
* TODO: void addTables(string[] $tables)
|
||||
* TODO: void addColumn(string $table, string $column)
|
||||
*
|
||||
* To see the latest implementation:
|
||||
*
|
||||
* @see ./SchemaHelper.php
|
||||
*/
|
||||
interface SchemaHelperInterface {
|
||||
|
||||
}
|
232
mixin/lib/civimix-schema@5.80.2/src/SqlGenerator.php
Normal file
232
mixin/lib/civimix-schema@5.80.2/src/SqlGenerator.php
Normal file
|
@ -0,0 +1,232 @@
|
|||
<?php
|
||||
/*
|
||||
+--------------------------------------------------------------------+
|
||||
| Copyright CiviCRM LLC. All rights reserved. |
|
||||
| |
|
||||
| This work is published under the GNU AGPLv3 license with some |
|
||||
| permitted exceptions and without any warranty. For full license |
|
||||
| and copyright information, see https://civicrm.org/licensing |
|
||||
+--------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
return new class() {
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $entities;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $findExternalTable;
|
||||
|
||||
/**
|
||||
* @param string $module
|
||||
* Ex: 'civicrm' or 'org.example.mymodule'
|
||||
* @param string $path
|
||||
* Ex: '/var/www/sites/all/modules/civicrm/schema'
|
||||
* @param bool $isolated
|
||||
* TRUE if these entities should be a self-sufficient (i.e. no external references).
|
||||
* FALSE if these entities may include references to other tables.
|
||||
* TRUE would make sense in (eg) civicrm-core, before installation or bootstrap
|
||||
* FALSE would make sense in (eg) an extension on an active system.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function createFromFolder(string $module, string $path, bool $isolated) {
|
||||
$files = \CRM_Utils_File::findFiles($path, '*.entityType.php');
|
||||
$entities = [];
|
||||
foreach ($files as $file) {
|
||||
$entity = include $file;
|
||||
$entity['module'] = $module;
|
||||
$entities[$entity['name']] = $entity;
|
||||
}
|
||||
|
||||
$findExternalTable = $isolated ? NULL : (['CRM_Core_DAO_AllCoreTables', 'getTableForEntityName']);
|
||||
return new static($entities, $findExternalTable);
|
||||
}
|
||||
|
||||
public function __construct(array $entities = [], ?callable $findExternalTable = NULL) {
|
||||
// Filter out entities without a sql table (e.g. Afform)
|
||||
$this->entities = array_filter($entities, function($entity) {
|
||||
return !empty($entity['table']);
|
||||
});
|
||||
$this->findExternalTable = $findExternalTable ?: function() {
|
||||
return NULL;
|
||||
};
|
||||
}
|
||||
|
||||
public function getEntities(): array {
|
||||
return $this->entities;
|
||||
}
|
||||
|
||||
public function getCreateTablesSql(): string {
|
||||
$sql = '';
|
||||
foreach ($this->entities as $entity) {
|
||||
$sql .= $this->generateCreateTableSql($entity);
|
||||
}
|
||||
foreach ($this->entities as $entity) {
|
||||
$sql .= $this->generateConstraintsSql($entity);
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
public function getCreateTableSql(string $entityName): string {
|
||||
$sql = $this->generateCreateTableSql($this->entities[$entityName]);
|
||||
$sql .= $this->generateConstraintsSql($this->entities[$entityName]);
|
||||
return $sql;
|
||||
}
|
||||
|
||||
public function getDropTablesSql(): string {
|
||||
$sql = "SET FOREIGN_KEY_CHECKS=0;\n";
|
||||
foreach ($this->entities as $entity) {
|
||||
$sql .= "DROP TABLE IF EXISTS `{$entity['table']}`;\n";
|
||||
}
|
||||
$sql .= "SET FOREIGN_KEY_CHECKS=1;\n";
|
||||
return $sql;
|
||||
}
|
||||
|
||||
public function generateCreateTableWithConstraintSql(array $entity): string {
|
||||
$definition = $this->getTableDefinition($entity);
|
||||
$constraints = $this->getTableConstraints($entity);
|
||||
$sql = "CREATE TABLE IF NOT EXISTS `{$entity['table']}` (\n " .
|
||||
implode(",\n ", $definition);
|
||||
if ($constraints) {
|
||||
$sql .= ",\n " . implode(",\n ", $constraints);
|
||||
}
|
||||
$sql .= "\n)\n" . $this->getTableOptions() . ";\n";
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private function generateCreateTableSql(array $entity): string {
|
||||
$definition = $this->getTableDefinition($entity);
|
||||
$sql = "CREATE TABLE `{$entity['table']}` (\n " .
|
||||
implode(",\n ", $definition) .
|
||||
"\n)\n" .
|
||||
$this->getTableOptions() . ";\n";
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private function getTableDefinition(array $entity): array {
|
||||
$definition = [];
|
||||
$primaryKeys = [];
|
||||
foreach ($entity['getFields']() as $fieldName => $field) {
|
||||
if (!empty($field['primary_key'])) {
|
||||
$primaryKeys[] = "`$fieldName`";
|
||||
}
|
||||
$definition[] = "`$fieldName` " . self::generateFieldSql($field);
|
||||
}
|
||||
if ($primaryKeys) {
|
||||
$definition[] = 'PRIMARY KEY (' . implode(', ', $primaryKeys) . ')';
|
||||
}
|
||||
$indices = isset($entity['getIndices']) ? $entity['getIndices']() : [];
|
||||
foreach ($indices as $indexName => $index) {
|
||||
$indexFields = [];
|
||||
foreach ($index['fields'] as $fieldName => $length) {
|
||||
$indexFields[] = "`$fieldName`" . (is_int($length) ? "($length)" : '');
|
||||
}
|
||||
$definition[] = (!empty($index['unique']) ? 'UNIQUE ' : '') . "INDEX `$indexName`(" . implode(', ', $indexFields) . ')';
|
||||
}
|
||||
return $definition;
|
||||
}
|
||||
|
||||
private function generateConstraintsSql(array $entity): string {
|
||||
$constraints = $this->getTableConstraints($entity);
|
||||
$sql = '';
|
||||
if ($constraints) {
|
||||
$sql .= "ALTER TABLE `{$entity['table']}`\n ";
|
||||
$sql .= 'ADD ' . implode(",\n ADD ", $constraints) . ";\n";
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private function getTableConstraints(array $entity): array {
|
||||
$constraints = [];
|
||||
foreach ($entity['getFields']() as $fieldName => $field) {
|
||||
if (!empty($field['entity_reference']['entity'])) {
|
||||
$constraint = "CONSTRAINT `FK_{$entity['table']}_$fieldName` FOREIGN KEY (`$fieldName`)" .
|
||||
" REFERENCES `" . $this->getTableForEntity($field['entity_reference']['entity']) . "`(`{$field['entity_reference']['key']}`)";
|
||||
if (!empty($field['entity_reference']['on_delete'])) {
|
||||
$constraint .= " ON DELETE {$field['entity_reference']['on_delete']}";
|
||||
}
|
||||
$constraints[] = $constraint;
|
||||
}
|
||||
}
|
||||
return $constraints;
|
||||
}
|
||||
|
||||
public static function generateFieldSql(array $field) {
|
||||
$fieldSql = $field['sql_type'];
|
||||
if (!empty($field['collate'])) {
|
||||
$fieldSql .= " COLLATE {$field['collate']}";
|
||||
}
|
||||
// Required fields and booleans cannot be null
|
||||
// FIXME: For legacy support this doesn't force boolean fields to be NOT NULL... but it really should.
|
||||
if (!empty($field['required'])) {
|
||||
$fieldSql .= ' NOT NULL';
|
||||
}
|
||||
else {
|
||||
$fieldSql .= ' NULL';
|
||||
}
|
||||
if (!empty($field['auto_increment'])) {
|
||||
$fieldSql .= " AUTO_INCREMENT";
|
||||
}
|
||||
$fieldSql .= self::getDefaultSql($field);
|
||||
if (!empty($field['description'])) {
|
||||
$fieldSql .= " COMMENT '" . \CRM_Core_DAO::escapeString($field['description']) . "'";
|
||||
}
|
||||
return $fieldSql;
|
||||
}
|
||||
|
||||
private static function getDefaultSql(array $field): string {
|
||||
// Booleans always have a default
|
||||
if ($field['sql_type'] === 'boolean') {
|
||||
$field += ['default' => FALSE];
|
||||
}
|
||||
if (!array_key_exists('default', $field)) {
|
||||
return '';
|
||||
}
|
||||
if (is_null($field['default'])) {
|
||||
$default = 'NULL';
|
||||
}
|
||||
elseif (is_bool($field['default'])) {
|
||||
$default = $field['default'] ? 'TRUE' : 'FALSE';
|
||||
}
|
||||
elseif (!is_string($field['default']) || str_starts_with($field['default'], 'CURRENT_TIMESTAMP')) {
|
||||
$default = $field['default'];
|
||||
}
|
||||
else {
|
||||
$default = "'" . \CRM_Core_DAO::escapeString($field['default']) . "'";
|
||||
}
|
||||
return ' DEFAULT ' . $default;
|
||||
}
|
||||
|
||||
private function getTableForEntity(string $entityName): string {
|
||||
return $this->entities[$entityName]['table'] ?? call_user_func($this->findExternalTable, $entityName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get general/default options for use in CREATE TABLE (eg character set, collation).
|
||||
*/
|
||||
private function getTableOptions(): string {
|
||||
if (!Civi\Core\Container::isContainerBooted()) {
|
||||
// Pre-installation environment ==> aka new install
|
||||
$collation = CRM_Core_BAO_SchemaHandler::DEFAULT_COLLATION;
|
||||
}
|
||||
else {
|
||||
// What character-set is used for CiviCRM core schema? What collation?
|
||||
// This depends on when the DB was *initialized*:
|
||||
// - civicrm-core >= 5.33 has used `CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`
|
||||
// - civicrm-core 4.3-5.32 has used `CHARACTER SET utf8 COLLATE utf8_unicode_ci`
|
||||
// - civicrm-core <= 4.2 -- I haven't checked, but it's probably the same.
|
||||
// Some systems have migrated (eg APIv3's `System.utf8conversion`), but (as of Feb 2024)
|
||||
// we haven't made any effort to push to this change.
|
||||
$collation = \CRM_Core_BAO_SchemaHandler::getInUseCollation();
|
||||
}
|
||||
|
||||
$characterSet = (stripos($collation, 'utf8mb4') !== FALSE) ? 'utf8mb4' : 'utf8';
|
||||
return "ENGINE=InnoDB DEFAULT CHARACTER SET {$characterSet} COLLATE {$collation} ROW_FORMAT=DYNAMIC";
|
||||
}
|
||||
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue