mirror of
https://codeberg.org/artfulrobot/contactcats.git
synced 2025-06-28 03:47:18 +02:00
232 lines
8.2 KiB
PHP
232 lines
8.2 KiB
PHP
<?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";
|
|
}
|
|
|
|
};
|