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";
|
||||
}
|
||||
|
||||
};
|
711
mixin/lib/pathload-0.php
Normal file
711
mixin/lib/pathload-0.php
Normal file
|
@ -0,0 +1,711 @@
|
|||
<?php
|
||||
//phpcs:disable
|
||||
/*
|
||||
PHP PathLoad (MIT License)
|
||||
Copyright (c) 2022-2024 CiviCRM LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
*/
|
||||
namespace {
|
||||
if (isset($GLOBALS['_PathLoad'][0])) {
|
||||
return $GLOBALS['_PathLoad'][0];
|
||||
}
|
||||
if (!interface_exists('PathLoadInterface')) {
|
||||
/**
|
||||
* The PathLoad interface is defined via soft signatures ("duck-typing") rather than hard signatures.
|
||||
* This matters when multiple parties inject PathLoad support onto a pre-existing framework.
|
||||
* In the event of future language changes or contract changes, the soft signatures
|
||||
* give wiggle-room to address interoperability/conversion.
|
||||
*
|
||||
* ==== PACKAGE CONSUMER APIS ===
|
||||
*
|
||||
* (PathLoad v0) Enable autoloading of `*.phar`, `*.php`, and folders from a search directory.
|
||||
*
|
||||
* @method PathLoadInterface addSearchDir(string $baseDir)
|
||||
*
|
||||
* (Pathload v0) Enable autoloading of a specific `*.phar`, `*.php`, or folder.
|
||||
* Useful for non-standard file-layout.
|
||||
*
|
||||
* @method PathLoadInterface addSearchItem(string $name, string $version, string $file, ?string $type = NULL)
|
||||
*
|
||||
* (PathLoad v0) Add auto-loading hints. If someone requests a class in $namespace, then we load $package.
|
||||
*
|
||||
* Consecutive/identical calls to addNamespace() are de-duplicated.
|
||||
*
|
||||
* @method PathLoadInterface addNamespace(string $package, $namespaces)
|
||||
*
|
||||
* (Pathload v0) When you need resources from a package, call loadPackage().
|
||||
* This locates the relevant files and loads them.
|
||||
* If you use namespace-autoloading, then this shouldn't be necessary.
|
||||
*
|
||||
* @method PathLoadInterface loadPackage(string $majorName)
|
||||
*
|
||||
* ==== PACKAGE PROVIDER APIS ====
|
||||
*
|
||||
* (PathLoad v0) Activate your package. This allows you to add metadata about activating
|
||||
* your own package. In particular, this may be necessary if you have transitive
|
||||
* dependencies. This would be appropriate for single-file PHP package (`cloud-io@1.0.0.php`)
|
||||
* which lack direct support for `pathload.json`.
|
||||
*
|
||||
* @method PathLoadInterface activatePackage(string $majorName, string $dir, array $config)
|
||||
*/
|
||||
interface PathLoadInterface {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace PathLoad\V0 {
|
||||
if (!class_exists('PathLoad')) {
|
||||
function doRequire(string $file) {
|
||||
require_once $file;
|
||||
}
|
||||
/**
|
||||
* Locate version-compliant instances of PathLoad.
|
||||
*/
|
||||
class Versions implements \ArrayAccess {
|
||||
public $top;
|
||||
public function __construct($top) {
|
||||
$this->top = $top;
|
||||
}
|
||||
public function offsetExists($version): bool {
|
||||
return ($version === 'top' || $version <= $this->top->version);
|
||||
}
|
||||
public function offsetGet($version): ?\PathLoadInterface {
|
||||
if ($version === 'top' || $version <= $this->top->version) {
|
||||
return $this->top;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
public function offsetSet($offset, $value): void {
|
||||
error_log("Cannot overwrite PathLoad[$offset]");
|
||||
}
|
||||
public function offsetUnset($offset): void {
|
||||
error_log("Cannot remove PathLoad[$offset]");
|
||||
}
|
||||
}
|
||||
class Package {
|
||||
/**
|
||||
* Split a package identifier into its parts.
|
||||
*
|
||||
* @param string $package
|
||||
* Ex: 'foobar@1.2.3'
|
||||
* @return array
|
||||
* Tuple: [$majorName, $name, $version]
|
||||
* Ex: 'foobar@1', 'foobar', '1.2.3'
|
||||
*/
|
||||
public static function parseExpr(string $package): array {
|
||||
if (strpos($package, '@') === FALSE) {
|
||||
throw new \RuntimeException("Malformed package name: $package");
|
||||
}
|
||||
[$prefix, $suffix] = explode('@', $package, 2);
|
||||
$prefix = str_replace('/', '~', $prefix);
|
||||
[$major] = explode('.', $suffix, 2);
|
||||
return ["$prefix@$major", $prefix, $suffix];
|
||||
}
|
||||
public static function parseFileType(string $file): array {
|
||||
if (substr($file, -4) === '.php') {
|
||||
return ['php', substr(basename($file), 0, -4)];
|
||||
}
|
||||
elseif (substr($file, '-5') === '.phar') {
|
||||
return ['phar', substr(basename($file), 0, -5)];
|
||||
}
|
||||
elseif (is_dir($file)) {
|
||||
return ['dir', basename($file)];
|
||||
}
|
||||
else {
|
||||
return [NULL, NULL];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param string $file
|
||||
* Ex: '/var/www/app-1/lib/foobar@.1.2.3.phar'
|
||||
* @return \PathLoad\Vn\Package|null
|
||||
*/
|
||||
public static function create(string $file): ?Package {
|
||||
[$type, $base] = self::parseFileType($file);
|
||||
if ($type === NULL) {
|
||||
return NULL;
|
||||
}
|
||||
$self = new Package();
|
||||
[$self->majorName, $self->name, $self->version] = static::parseExpr($base);
|
||||
$self->file = $file;
|
||||
$self->type = $type;
|
||||
return $self;
|
||||
}
|
||||
/**
|
||||
* @var string
|
||||
* Ex: '/var/www/app-1/lib/cloud-file-io@1.2.3.phar'
|
||||
*/
|
||||
public $file;
|
||||
/**
|
||||
* @var string
|
||||
* Ex: 'cloud-file-io'
|
||||
*/
|
||||
public $name;
|
||||
/**
|
||||
* @var string
|
||||
* Ex: 'cloud-file-io@1'
|
||||
*/
|
||||
public $majorName;
|
||||
/**
|
||||
* @var string
|
||||
* Ex: '1.2.3'
|
||||
*/
|
||||
public $version;
|
||||
/**
|
||||
* @var string
|
||||
* Ex: 'php' or 'phar' or 'dir'
|
||||
*/
|
||||
public $type;
|
||||
public $reloadable = FALSE;
|
||||
}
|
||||
class Scanner {
|
||||
/**
|
||||
* @var array
|
||||
* Array(string $id => [package => string, glob => string])
|
||||
* @internal
|
||||
*/
|
||||
public $allRules = [];
|
||||
/**
|
||||
* @var array
|
||||
* Array(string $id => [package => string, glob => string])
|
||||
* @internal
|
||||
*/
|
||||
public $newRules = [];
|
||||
/**
|
||||
* @param array $rule
|
||||
* Ex: ['package' => '*', 'glob' => '/var/www/lib/*@*']
|
||||
* Ex: ['package' => 'cloud-file-io@1', 'glob' => '/var/www/lib/cloud-io@1*.phar'])
|
||||
* @return void
|
||||
*/
|
||||
public function addRule(array $rule): void {
|
||||
$id = static::id($rule);
|
||||
$this->newRules[$id] = $this->allRules[$id] = $rule;
|
||||
}
|
||||
public function reset(): void {
|
||||
$this->newRules = $this->allRules;
|
||||
}
|
||||
/**
|
||||
* Evaluate any rules that have a chance of finding $packageHint.
|
||||
*
|
||||
* @param string $packageHint
|
||||
* Give a hint about what package we're looking for.
|
||||
* The scanner will try to target packages based on this hint.
|
||||
* Ex: '*' or 'cloud-file-io'
|
||||
* @return \Generator
|
||||
* A list of packages. These may not be the exact package you're looking for.
|
||||
* You should assimilate knowledge of all outputs because you may not get them again.
|
||||
*/
|
||||
public function scan(string $packageHint): \Generator {
|
||||
yield from [];
|
||||
foreach (array_keys($this->newRules) as $id) {
|
||||
$searchRule = $this->newRules[$id];
|
||||
if ($searchRule['package'] === '*' || $searchRule['package'] === $packageHint) {
|
||||
unset($this->newRules[$id]);
|
||||
if (isset($searchRule['glob'])) {
|
||||
foreach ((array) glob($searchRule['glob']) as $file) {
|
||||
if (($package = Package::create($file)) !== NULL) {
|
||||
yield $package;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($searchRule['file'])) {
|
||||
$package = new Package();
|
||||
$package->file = $searchRule['file'];
|
||||
$package->name = $searchRule['package'];
|
||||
$package->majorName = $searchRule['package'] . '@' . explode('.', $searchRule['version'])[0];
|
||||
$package->version = $searchRule['version'];
|
||||
$package->type = $searchRule['type'] ?: Package::parseFileType($searchRule['file'])[0];
|
||||
yield $package;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
protected static function id(array $rule): string {
|
||||
if (isset($rule['glob'])) {
|
||||
return $rule['glob'];
|
||||
}
|
||||
elseif (isset($rule['file'])) {
|
||||
return md5(implode(' ', [$rule['file'], $rule['package'], $rule['version']]));
|
||||
}
|
||||
else {
|
||||
throw new \RuntimeException("Cannot identify rule: " . json_encode($rule));
|
||||
}
|
||||
}
|
||||
}
|
||||
class Psr0Loader {
|
||||
/**
|
||||
* @var array
|
||||
* Ex: $paths['F']['Foo_'][0] = '/var/www/app/lib/foo@1.0.0/src/';
|
||||
* @internal
|
||||
*/
|
||||
public $paths = [];
|
||||
/**
|
||||
* @param string $dir
|
||||
* @param array $config
|
||||
* Ex: ['Foo_' => ['src/']] or ['Foo_' => ['Foo_']]
|
||||
*/
|
||||
public function addAll(string $dir, array $config) {
|
||||
$dir = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
||||
foreach ($config as $prefix => $relPaths) {
|
||||
$bucket = $prefix[0];
|
||||
foreach ((array) $relPaths as $relPath) {
|
||||
$this->paths[$bucket][$prefix][] = $dir . $relPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Loads the class file for a given class name.
|
||||
*
|
||||
* @param string $class The fully-qualified class name.
|
||||
* @return mixed The mapped file name on success, or boolean false on failure.
|
||||
*/
|
||||
public function loadClass(string $class) {
|
||||
$bucket = $class[0];
|
||||
if (!isset($this->paths[$bucket])) {
|
||||
return FALSE;
|
||||
}
|
||||
$file = DIRECTORY_SEPARATOR . str_replace(['_', '\\'], [DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR], $class) . '.php';
|
||||
foreach ($this->paths[$bucket] as $prefix => $paths) {
|
||||
if ($prefix === substr($class, 0, strlen($prefix))) {
|
||||
foreach ($paths as $path) {
|
||||
$fullFile = $path . $file;
|
||||
if (file_exists($fullFile)) {
|
||||
doRequire($fullFile);
|
||||
return $fullFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
class Psr4Loader {
|
||||
/**
|
||||
* @var array
|
||||
* Ex: $prefixes['Foo\\'][0] = '/var/www/app/lib/foo@1.0.0/src/']
|
||||
* @internal
|
||||
*/
|
||||
public $prefixes = [];
|
||||
public function addAll(string $dir, array $config) {
|
||||
foreach ($config as $prefix => $relPaths) {
|
||||
foreach ($relPaths as $relPath) {
|
||||
$this->addNamespace($prefix, $dir . '/' . $relPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Adds a base directory for a namespace prefix.
|
||||
*
|
||||
* @param string $prefix
|
||||
* The namespace prefix.
|
||||
* @param string $baseDir
|
||||
* A base directory for class files in the namespace.
|
||||
* @return void
|
||||
*/
|
||||
private function addNamespace($prefix, $baseDir) {
|
||||
$prefix = trim($prefix, '\\') . '\\';
|
||||
$baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . '/';
|
||||
if (isset($this->prefixes[$prefix]) === FALSE) {
|
||||
$this->prefixes[$prefix] = [];
|
||||
}
|
||||
array_push($this->prefixes[$prefix], $baseDir);
|
||||
}
|
||||
/**
|
||||
* Loads the class file for a given class name.
|
||||
*
|
||||
* @param string $class The fully-qualified class name.
|
||||
* @return mixed The mapped file name on success, or boolean false on failure.
|
||||
*/
|
||||
public function loadClass(string $class) {
|
||||
$prefix = $class;
|
||||
while (FALSE !== $pos = strrpos($prefix, '\\')) {
|
||||
$prefix = substr($class, 0, $pos + 1);
|
||||
$relativeClass = substr($class, $pos + 1);
|
||||
if ($mappedFile = $this->findRelativeClass($prefix, $relativeClass)) {
|
||||
doRequire($mappedFile);
|
||||
return $mappedFile;
|
||||
}
|
||||
$prefix = rtrim($prefix, '\\');
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
/**
|
||||
* Load the mapped file for a namespace prefix and relative class.
|
||||
*
|
||||
* @param string $prefix
|
||||
* The namespace prefix.
|
||||
* @param string $relativeClass
|
||||
* The relative class name.
|
||||
* @return string|FALSE
|
||||
* Matched file name, or FALSE if none found.
|
||||
*/
|
||||
private function findRelativeClass($prefix, $relativeClass) {
|
||||
if (isset($this->prefixes[$prefix]) === FALSE) {
|
||||
return FALSE;
|
||||
}
|
||||
$relFile = str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php';
|
||||
foreach ($this->prefixes[$prefix] as $baseDir) {
|
||||
$file = $baseDir . $relFile;
|
||||
if (file_exists($file)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
class PathLoad implements \PathLoadInterface {
|
||||
/**
|
||||
* @var null|int
|
||||
*/
|
||||
public $version;
|
||||
/**
|
||||
* @var Scanner
|
||||
* @internal
|
||||
*/
|
||||
public $scanner;
|
||||
/**
|
||||
* List of best-known versions for each package.
|
||||
*
|
||||
* Packages are loaded lazily. Once loaded, the data is moved to $loadedPackages.
|
||||
*
|
||||
* @var Package[]
|
||||
* Ex: ['cloud-file-io@1' => new Package('/usr/share/php-pathload/cloud-file-io@1.2.3.phar',
|
||||
* ...)]
|
||||
* @internal
|
||||
*/
|
||||
public $availablePackages = [];
|
||||
/**
|
||||
* List of packages that have already been resolved.
|
||||
*
|
||||
* @var Package[]
|
||||
* Ex: ['cloud-file-io@1' => new Package('/usr/share/php-pathload/cloud-file-io@1.2.3.phar',
|
||||
* ...)] Note: If PathLoad version is super-ceded, then the loadedPackages may be instances of
|
||||
* an old `Package` class. Be mindful of duck-type compatibility. We don't strictly need to
|
||||
* retain this data, but it feels it'd be handy for debugging.
|
||||
* @internal
|
||||
*/
|
||||
public $loadedPackages = [];
|
||||
/**
|
||||
* Log of package activations. Used to re-initialize class-loader if we upgrade.
|
||||
*
|
||||
* @var array
|
||||
* @internal
|
||||
*/
|
||||
public $activatedPackages = [];
|
||||
/**
|
||||
* List of hints for class-loading. If someone tries to use a matching class, then
|
||||
* load the corresponding package.
|
||||
*
|
||||
* Namespace-rules are evaluated lazily. Once evaluated, the data is removed.
|
||||
*
|
||||
* @var array
|
||||
* Array(string $prefix => [string $package => string $package])
|
||||
* Ex: ['Super\Cloud\IO\' => ['cloud-io@1' => 'cloud-io@1']
|
||||
* @internal
|
||||
*/
|
||||
public $availableNamespaces;
|
||||
/**
|
||||
* @var \PathLoad\Vn\Psr0Loader
|
||||
* @internal
|
||||
*/
|
||||
public $psr0;
|
||||
/**
|
||||
* @var \PathLoad\Vn\Psr4Loader
|
||||
* @internal
|
||||
*/
|
||||
public $psr4;
|
||||
/**
|
||||
* @param int $version
|
||||
* Identify the version being instantiated.
|
||||
* @param \PathLoadInterface|null $old
|
||||
* If this instance is a replacement for an older instance, then it will be passed in.
|
||||
* @return \ArrayAccess
|
||||
* Versioned work-a-like array.
|
||||
*/
|
||||
public static function create(int $version, ?\PathLoadInterface $old = NULL) {
|
||||
if ($old !== NULL) {
|
||||
$old->unregister();
|
||||
}
|
||||
$new = new static();
|
||||
$new->version = $version;
|
||||
$new->scanner = new Scanner();
|
||||
$new->psr0 = new Psr0Loader();
|
||||
$new->psr4 = new Psr4Loader();
|
||||
$new->register();
|
||||
// The exact protocol for assimilating $old instances may need change.
|
||||
// This seems like a fair guess as long as old properties are forward-compatible.
|
||||
|
||||
if ($old === NULL) {
|
||||
$baseDirs = getenv('PHP_PATHLOAD') ? explode(PATH_SEPARATOR, getenv('PHP_PATHLOAD')) : [];
|
||||
foreach ($baseDirs as $baseDir) {
|
||||
$new->addSearchDir($baseDir);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// TIP: You might use $old->version to decide what to use.
|
||||
foreach ($old->scanner->allRules as $rule) {
|
||||
$new->scanner->addRule($rule);
|
||||
}
|
||||
$new->loadedPackages = $old->loadedPackages;
|
||||
$new->availableNamespaces = $old->availableNamespaces;
|
||||
foreach ($old->activatedPackages as $activatedPackage) {
|
||||
$new->activatePackage($activatedPackage['name'], $activatedPackage['dir'], $activatedPackage['config']);
|
||||
}
|
||||
}
|
||||
return new Versions($new);
|
||||
}
|
||||
public function register(): \PathLoadInterface {
|
||||
spl_autoload_register([$this, 'loadClass']);
|
||||
return $this;
|
||||
}
|
||||
public function unregister(): \PathLoadInterface {
|
||||
spl_autoload_unregister([$this, 'loadClass']);
|
||||
return $this;
|
||||
}
|
||||
public function reset(): \PathLoadInterface {
|
||||
$this->scanner->reset();
|
||||
return $this;
|
||||
}
|
||||
/**
|
||||
* Append a directory (with many packages) to the search-path.
|
||||
*
|
||||
* @param string $baseDir
|
||||
* The path to a base directory (e.g. `/var/www/myapp/lib`) which contains many packages (e.g.
|
||||
* `foo@1.2.3.phar` or `bar@4.5.6/autoload.php`).
|
||||
*/
|
||||
public function addSearchDir(string $baseDir): \PathLoadInterface {
|
||||
$this->scanner->addRule(['package' => '*', 'glob' => "$baseDir/*@*"]);
|
||||
return $this;
|
||||
}
|
||||
/**
|
||||
* Append one specific item to the search list.
|
||||
*
|
||||
* @param string $name
|
||||
* Ex: 'cloud-file-io'
|
||||
* @param string $version
|
||||
* Ex: '1.2.3'
|
||||
* @param string $file
|
||||
* Full path to the file or folder.
|
||||
* @param string|null $type
|
||||
* One of: 'php', 'phar', or 'dir'. NULL will auto-detect.
|
||||
*
|
||||
* @return \PathLoadInterface
|
||||
*/
|
||||
public function addSearchItem(string $name, string $version, string $file, ?string $type = NULL): \PathLoadInterface {
|
||||
$this->scanner->addRule(['package' => $name, 'version' => $version, 'file' => $file, 'type' => $type]);
|
||||
return $this;
|
||||
}
|
||||
/**
|
||||
* Add auto-loading hints. If someone requests a class in $namespace, then we load $package.
|
||||
*
|
||||
* Consecutive/identical calls to addNamespace() are de-duplicated.
|
||||
*
|
||||
* @param string $package
|
||||
* Ex: 'cloud-io@1'
|
||||
* @param string|string[] $namespaces
|
||||
* Ex: 'Super\Cloud\IO\'
|
||||
*/
|
||||
public function addNamespace(string $package, $namespaces): \PathLoadInterface {
|
||||
foreach ((array) $namespaces as $namespace) {
|
||||
$this->availableNamespaces[$namespace][$package] = $package;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
public function loadClass(string $class) {
|
||||
if (strpos($class, '\\') !== FALSE) {
|
||||
$this->loadPackagesByNamespace('\\', explode('\\', $class));
|
||||
}
|
||||
elseif (strpos($class, '_') !== FALSE) {
|
||||
$this->loadPackagesByNamespace('_', explode('_', $class));
|
||||
}
|
||||
return $this->psr4->loadClass($class) || $this->psr0->loadClass($class);
|
||||
}
|
||||
/**
|
||||
* If the application requests class "Foo\Bar\Whiz\Bang", then you should load
|
||||
* any packages related to "Foo\*", "Foo\Bar\*", or "Foo\Bar\Whiz\*".
|
||||
*
|
||||
* @param string $delim
|
||||
* Ex: '\\' or '_'
|
||||
* @param string[] $classParts
|
||||
* Ex: ['Symfony', 'Components', 'Filesystem', 'Filesystem']
|
||||
*/
|
||||
private function loadPackagesByNamespace(string $delim, array $classParts): void {
|
||||
array_pop($classParts);
|
||||
do {
|
||||
$foundPackages = FALSE;
|
||||
$namespace = '';
|
||||
foreach ($classParts as $nsPart) {
|
||||
$namespace .= $nsPart . $delim;
|
||||
if (isset($this->availableNamespaces[$namespace])) {
|
||||
$packages = $this->availableNamespaces[$namespace];
|
||||
foreach ($packages as $package) {
|
||||
unset($this->availableNamespaces[$namespace][$package]);
|
||||
if ($this->loadPackage($package)) {
|
||||
$foundPackages = TRUE;
|
||||
}
|
||||
else {
|
||||
trigger_error("PathLoad: Failed to locate package \"$package\" required for namespace \"$namespace\"", E_USER_WARNING);
|
||||
$this->availableNamespaces[$namespace][$package] = $package; /* Maybe some other time */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while ($foundPackages);
|
||||
// Loading a package could produce metadata about other packages. Assimilate those too.
|
||||
}
|
||||
/**
|
||||
* Load the content of a package.
|
||||
*
|
||||
* @param string $majorName
|
||||
* Ex: 'cloud-io@1'
|
||||
* @param bool $reload
|
||||
* @return string|NULL
|
||||
* The version# of the loaded package. Otherwise, NULL
|
||||
*/
|
||||
public function loadPackage(string $majorName, bool $reload = FALSE): ?string {
|
||||
if (isset($this->loadedPackages[$majorName])) {
|
||||
if ($reload && $this->loadedPackages[$majorName]->reloadable) {
|
||||
$this->scanner->reset();
|
||||
}
|
||||
else {
|
||||
if ($reload) {
|
||||
trigger_error("PathLoad: Declined to reload \"$majorName\". Package is not reloadable.", E_USER_WARNING);
|
||||
}
|
||||
return $this->loadedPackages[$majorName]->version;
|
||||
}
|
||||
}
|
||||
$this->scanAvailablePackages(explode('@', $majorName, 2)[0], $this->availablePackages);
|
||||
if (!isset($this->availablePackages[$majorName])) {
|
||||
return NULL;
|
||||
}
|
||||
$package = $this->loadedPackages[$majorName] = $this->availablePackages[$majorName];
|
||||
unset($this->availablePackages[$majorName]);
|
||||
switch ($package->type ?? NULL) {
|
||||
case 'php':
|
||||
doRequire($package->file);
|
||||
return $package->version;
|
||||
case 'phar':
|
||||
doRequire($package->file);
|
||||
$this->useMetadataFiles($package, 'phar://' . $package->file);
|
||||
return $package->version;
|
||||
case 'dir':
|
||||
$this->useMetadataFiles($package, $package->file);
|
||||
return $package->version;
|
||||
default:
|
||||
\error_log("PathLoad: Package (\"$majorName\") appears malformed.");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
private function scanAvailablePackages(string $hint, array &$avail): void {
|
||||
foreach ($this->scanner->scan($hint) as $package) {
|
||||
/** @var Package $package */
|
||||
if (!isset($avail[$package->majorName]) || \version_compare($package->version, $avail[$package->majorName]->version, '>')) {
|
||||
$avail[$package->majorName] = $package;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* When loading a package, execute metadata files like "pathload.main.php" or "pathload.json".
|
||||
*
|
||||
* @param Package $package
|
||||
* @param string $dir
|
||||
* Ex: '/var/www/lib/cloud-io@1.2.0'
|
||||
* Ex: 'phar:///var/www/lib/cloud-io@1.2.0.phar'
|
||||
*/
|
||||
private function useMetadataFiles(Package $package, string $dir): void {
|
||||
$phpFile = "$dir/pathload.main.php";
|
||||
$jsonFile = "$dir/pathload.json";
|
||||
if (file_exists($phpFile)) {
|
||||
require $phpFile;
|
||||
}
|
||||
elseif (file_exists($jsonFile)) {
|
||||
$jsonData = json_decode(file_get_contents($jsonFile), TRUE);
|
||||
$id = $package->name . '@' . $package->version;
|
||||
$this->activatePackage($id, $dir, $jsonData);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Given a configuration for the package, activate the correspond autoloader rules.
|
||||
*
|
||||
* @param string $majorName
|
||||
* Ex: 'cloud-io@1'
|
||||
* @param string|null $dir
|
||||
* Used for applying the 'autoload' rules.
|
||||
* Ex: '/var/www/lib/cloud-io@1.2.3'
|
||||
* @param array $config
|
||||
* Ex: ['autoload' => ['psr4' => ...], 'require-namespace' => [...], 'require-package' => [...]]
|
||||
* @return \PathLoadInterface
|
||||
*/
|
||||
public function activatePackage(string $majorName, ?string $dir, array $config): \PathLoadInterface {
|
||||
if (isset($config['reloadable'])) {
|
||||
$this->loadedPackages[$majorName]->reloadable = $config['reloadable'];
|
||||
}
|
||||
if (!isset($config['autoload'])) {
|
||||
return $this;
|
||||
}
|
||||
if ($dir === NULL) {
|
||||
throw new \RuntimeException("Cannot activate package $majorName. The 'autoload' property requires a base-directory.");
|
||||
}
|
||||
$this->activatedPackages[] = ['name' => $majorName, 'dir' => $dir, 'config' => $config];
|
||||
if (!empty($config['autoload']['include'])) {
|
||||
foreach ($config['autoload']['include'] as $file) {
|
||||
doRequire($dir . DIRECTORY_SEPARATOR . $file);
|
||||
}
|
||||
}
|
||||
if (isset($config['autoload']['psr-0'])) {
|
||||
$this->psr0->addAll($dir, $config['autoload']['psr-0']);
|
||||
}
|
||||
if (isset($config['autoload']['psr-4'])) {
|
||||
$this->psr4->addAll($dir, $config['autoload']['psr-4']);
|
||||
}
|
||||
foreach ($config['require-namespace'] ?? [] as $nsRule) {
|
||||
foreach ((array) $nsRule['package'] as $package) {
|
||||
foreach ((array) $nsRule['prefix'] as $prefix) {
|
||||
$this->availableNamespaces[$prefix][$package] = $package;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($config['require-package'] ?? [] as $package) {
|
||||
$this->loadPackage($package);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// New or upgraded instance.
|
||||
$GLOBALS['_PathLoad'] = \PathLoad\V0\PathLoad::create(0, $GLOBALS['_PathLoad']['top'] ?? NULL);
|
||||
if (!function_exists('pathload')) {
|
||||
/**
|
||||
* Get a reference the PathLoad manager.
|
||||
*
|
||||
* @param int|string $version
|
||||
* @return \PathLoadInterface
|
||||
*/
|
||||
function pathload($version = 'top') {
|
||||
return $GLOBALS['_PathLoad'][$version];
|
||||
}
|
||||
}
|
||||
return pathload();
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue