grand refactoring of schema and other database related things
This commit is contained in:
@@ -33,13 +33,13 @@ use Espo\Core\Utils\Config\MissingDefaultParamsSaver as ConfigMissingDefaultPara
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\ORM\EntityManagerProxy;
|
||||
use Espo\Core\Utils\Database\Schema\SchemaManagerProxy;
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Config\ConfigWriter;
|
||||
use Espo\Core\Utils\Metadata\OrmMetadataData;
|
||||
use Espo\Core\Utils\Database\Schema\SchemaProxy;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Utils\Module;
|
||||
use Espo\Core\Rebuild\RebuildActionProcessor;
|
||||
@@ -51,63 +51,37 @@ use Throwable;
|
||||
*/
|
||||
class DataManager
|
||||
{
|
||||
private Config $config;
|
||||
private ConfigWriter $configWriter;
|
||||
private EntityManagerProxy $entityManager;
|
||||
private Metadata $metadata;
|
||||
private OrmMetadataData $ormMetadataData;
|
||||
private HookManager $hookManager;
|
||||
private SchemaProxy $schemaProxy;
|
||||
private Log $log;
|
||||
private Module $module;
|
||||
private RebuildActionProcessor $rebuildActionProcessor;
|
||||
private ConfigMissingDefaultParamsSaver $configMissingDefaultParamsSaver;
|
||||
private FileManager $fileManager;
|
||||
|
||||
private string $cachePath = 'data/cache';
|
||||
|
||||
public function __construct(
|
||||
EntityManagerProxy $entityManager,
|
||||
Config $config,
|
||||
ConfigWriter $configWriter,
|
||||
Metadata $metadata,
|
||||
OrmMetadataData $ormMetadataData,
|
||||
HookManager $hookManager,
|
||||
SchemaProxy $schemaProxy,
|
||||
Log $log,
|
||||
Module $module,
|
||||
RebuildActionProcessor $rebuildActionProcessor,
|
||||
ConfigMissingDefaultParamsSaver $configMissingDefaultParamsSaver,
|
||||
FileManager $fileManager
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->config = $config;
|
||||
$this->configWriter = $configWriter;
|
||||
$this->metadata = $metadata;
|
||||
$this->ormMetadataData = $ormMetadataData;
|
||||
$this->hookManager = $hookManager;
|
||||
$this->schemaProxy = $schemaProxy;
|
||||
$this->log = $log;
|
||||
$this->module = $module;
|
||||
$this->rebuildActionProcessor = $rebuildActionProcessor;
|
||||
$this->configMissingDefaultParamsSaver = $configMissingDefaultParamsSaver;
|
||||
$this->fileManager = $fileManager;
|
||||
}
|
||||
private EntityManagerProxy $entityManager,
|
||||
private Config $config,
|
||||
private ConfigWriter $configWriter,
|
||||
private Metadata $metadata,
|
||||
private OrmMetadataData $ormMetadataData,
|
||||
private HookManager $hookManager,
|
||||
private SchemaManagerProxy $schemaManager,
|
||||
private Log $log,
|
||||
private Module $module,
|
||||
private RebuildActionProcessor $rebuildActionProcessor,
|
||||
private ConfigMissingDefaultParamsSaver $configMissingDefaultParamsSaver,
|
||||
private FileManager $fileManager
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Rebuild the system with metadata, database and cache clearing.
|
||||
*
|
||||
* @param ?string[] $entityList
|
||||
* @param ?string[] $entityTypeList
|
||||
* @throws Error
|
||||
*/
|
||||
public function rebuild(?array $entityList = null): void
|
||||
public function rebuild(?array $entityTypeList = null): void
|
||||
{
|
||||
$this->clearCache();
|
||||
$this->disableHooks();
|
||||
$this->checkModules();
|
||||
$this->rebuildMetadata();
|
||||
$this->populateConfigParameters();
|
||||
$this->rebuildDatabase($entityList);
|
||||
$this->rebuildDatabase($entityTypeList);
|
||||
$this->rebuildActionProcessor->process();
|
||||
$this->configMissingDefaultParamsSaver->process();
|
||||
$this->enableHooks();
|
||||
@@ -134,15 +108,15 @@ class DataManager
|
||||
/**
|
||||
* Rebuild database.
|
||||
*
|
||||
* @param ?string[] $entityList
|
||||
* @param ?string[] $entityTypeList
|
||||
* @throws Error
|
||||
*/
|
||||
public function rebuildDatabase(?array $entityList = null): void
|
||||
public function rebuildDatabase(?array $entityTypeList = null): void
|
||||
{
|
||||
$schema = $this->schemaProxy;
|
||||
$schemaManager = $this->schemaManager;
|
||||
|
||||
try {
|
||||
$result = $schema->rebuild($entityList);
|
||||
$result = $schemaManager->rebuild($entityTypeList);
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
$result = false;
|
||||
@@ -157,7 +131,7 @@ class DataManager
|
||||
throw new Error("Error while rebuilding database. See log file for details.");
|
||||
}
|
||||
|
||||
$databaseType = strtolower($schema->getDatabaseHelper()->getDatabaseType());
|
||||
$databaseType = strtolower($schemaManager->getDatabaseHelper()->getType());
|
||||
|
||||
if (
|
||||
!$this->config->get('actualDatabaseType') ||
|
||||
@@ -166,7 +140,7 @@ class DataManager
|
||||
$this->configWriter->set('actualDatabaseType', $databaseType);
|
||||
}
|
||||
|
||||
$databaseVersion = $schema->getDatabaseHelper()->getDatabaseVersion();
|
||||
$databaseVersion = $schemaManager->getDatabaseHelper()->getVersion();
|
||||
|
||||
if (
|
||||
!$this->config->get('actualDatabaseVersion') ||
|
||||
@@ -181,24 +155,14 @@ class DataManager
|
||||
|
||||
/**
|
||||
* Rebuild metadata.
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public function rebuildMetadata(): void
|
||||
{
|
||||
$metadata = $this->metadata;
|
||||
|
||||
$metadata->init(true);
|
||||
|
||||
$ormData = $this->ormMetadataData->getData(true);
|
||||
|
||||
$this->metadata->init(true);
|
||||
$this->ormMetadataData->reload();
|
||||
$this->entityManager->getMetadata()->updateData();
|
||||
|
||||
$this->updateCacheTimestamp();
|
||||
|
||||
if (empty($ormData)) {
|
||||
throw new Error("Error while rebuilding metadata. See log file for details.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\ORM;
|
||||
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\ORM\DatabaseParams;
|
||||
use RuntimeException;
|
||||
|
||||
class DatabaseParamsFactory
|
||||
{
|
||||
private const DEFAULT_PLATFORM = 'Mysql';
|
||||
private const DEFAULT_CHARSET = 'utf8';
|
||||
|
||||
public function __construct(private Config $config) {}
|
||||
|
||||
public function create(): DatabaseParams
|
||||
{
|
||||
$config = $this->config;
|
||||
|
||||
if (!$config->get('database')) {
|
||||
throw new RuntimeException('No database params in config.');
|
||||
}
|
||||
|
||||
$databaseParams = DatabaseParams::create()
|
||||
->withHost($config->get('database.host'))
|
||||
->withPort($config->get('database.port') ? (int) $config->get('database.port') : null)
|
||||
->withName($config->get('database.dbname'))
|
||||
->withUsername($config->get('database.user'))
|
||||
->withPassword($config->get('database.password'))
|
||||
->withCharset($config->get('database.charset') ?? self::DEFAULT_CHARSET)
|
||||
->withPlatform($config->get('database.platform'))
|
||||
->withSslCa($config->get('database.sslCA'))
|
||||
->withSslCert($config->get('database.sslCert'))
|
||||
->withSslKey($config->get('database.sslKey'))
|
||||
->withSslCaPath($config->get('database.sslCAPath'))
|
||||
->withSslCipher($config->get('database.sslCipher'))
|
||||
->withSslVerifyDisabled($config->get('database.sslVerifyDisabled') ?? false);
|
||||
|
||||
if (!$databaseParams->getPlatform()) {
|
||||
$databaseParams = $databaseParams->withPlatform(self::DEFAULT_PLATFORM);
|
||||
}
|
||||
|
||||
return $databaseParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
public function createWithMergedAssoc(array $params): DatabaseParams
|
||||
{
|
||||
$configParams = $this->create();
|
||||
|
||||
return DatabaseParams::create()
|
||||
->withHost($params['host'] ?? $configParams->getHost())
|
||||
->withPort(isset($params['port']) ? (int) $params['port'] : $configParams->getPort())
|
||||
->withName($params['dbname'] ?? $configParams->getName())
|
||||
->withUsername($params['user'] ?? $configParams->getUsername())
|
||||
->withPassword($params['password'] ?? $configParams->getPassword())
|
||||
->withCharset($params['charset'] ?? $configParams->getCharset())
|
||||
->withPlatform($params['platform'] ?? $configParams->getPlatform())
|
||||
->withSslCa($params['sslCA'] ?? $configParams->getSslCa())
|
||||
->withSslCert($params['sslCert'] ?? $configParams->getSslCert())
|
||||
->withSslKey($params['sslKey'] ?? $configParams->getSslKey())
|
||||
->withSslCaPath($params['sslCAPath'] ?? $configParams->getSslCaPath())
|
||||
->withSslCipher($params['sslCipher'] ?? $configParams->getSslCipher())
|
||||
->withSslVerifyDisabled($params['sslVerifyDisabled'] ?? $configParams->isSslVerifyDisabled());
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ namespace Espo\Core\ORM;
|
||||
|
||||
use Espo\Core\ORM\PDO\PDOFactoryFactory;
|
||||
use Espo\Core\ORM\QueryComposer\QueryComposerFactory;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\ORM\QueryComposer\Part\FunctionConverterFactory;
|
||||
@@ -52,18 +51,12 @@ use RuntimeException;
|
||||
|
||||
class EntityManagerFactory
|
||||
{
|
||||
/** @var array<string, string> */
|
||||
private $driverPlatformMap = [
|
||||
'pdo_mysql' => 'Mysql',
|
||||
'mysqli' => 'Mysql',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private MetadataDataProvider $metadataDataProvider,
|
||||
private EventDispatcher $eventDispatcher,
|
||||
private PDOFactoryFactory $pdoFactoryFactory
|
||||
private PDOFactoryFactory $pdoFactoryFactory,
|
||||
private DatabaseParamsFactory $databaseParamsFactory
|
||||
) {}
|
||||
|
||||
public function create(): EntityManager
|
||||
@@ -140,41 +133,10 @@ class EntityManagerFactory
|
||||
|
||||
private function createDatabaseParams(): DatabaseParams
|
||||
{
|
||||
$config = $this->config;
|
||||
|
||||
$databaseParams = DatabaseParams::create()
|
||||
->withHost($config->get('database.host'))
|
||||
->withPort($config->get('database.port') ? (int) $config->get('database.port') : null)
|
||||
->withName($config->get('database.dbname'))
|
||||
->withUsername($config->get('database.user'))
|
||||
->withPassword($config->get('database.password'))
|
||||
->withCharset($config->get('database.charset') ?? 'utf8')
|
||||
->withPlatform($config->get('database.platform'))
|
||||
->withSslCa($config->get('database.sslCA'))
|
||||
->withSslCert($config->get('database.sslCert'))
|
||||
->withSslKey($config->get('database.sslKey'))
|
||||
->withSslCaPath($config->get('database.sslCAPath'))
|
||||
->withSslCipher($config->get('database.sslCipher'))
|
||||
->withSslVerifyDisabled($config->get('database.sslVerifyDisabled') ?? false);
|
||||
$databaseParams = $this->databaseParamsFactory->create();
|
||||
|
||||
if (!$databaseParams->getName()) {
|
||||
throw new RuntimeException('No database name specified.');
|
||||
}
|
||||
|
||||
if (!$databaseParams->getPlatform()) {
|
||||
$driver = $config->get('database.driver');
|
||||
|
||||
if (!$driver) {
|
||||
throw new RuntimeException('No database driver specified.');
|
||||
}
|
||||
|
||||
$platform = $this->driverPlatformMap[$driver] ?? null;
|
||||
|
||||
if (!$platform) {
|
||||
throw new RuntimeException("Database driver '{$driver}' is not supported.");
|
||||
}
|
||||
|
||||
$databaseParams = $databaseParams->withPlatform($platform);
|
||||
throw new RuntimeException('No database name specified in config.');
|
||||
}
|
||||
|
||||
return $databaseParams;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Rebuild\Actions;
|
||||
|
||||
use Espo\Core\ApplicationUser;
|
||||
use Espo\Core\Rebuild\RebuildAction;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class AddSystemUser implements RebuildAction
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Config $config
|
||||
) {}
|
||||
|
||||
public function process(): void
|
||||
{
|
||||
$userId = ApplicationUser::SYSTEM_USER_ID;
|
||||
|
||||
$repository = $this->entityManager->getRDBRepositoryByClass(User::class);
|
||||
|
||||
$user = $repository->getById($userId);
|
||||
|
||||
if ($user) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var array<string, mixed> $attributes */
|
||||
$attributes = $this->config->get('systemUserAttributes');
|
||||
|
||||
$user = $repository->getNew();
|
||||
$user->set($attributes);
|
||||
$repository->save($user);
|
||||
}
|
||||
}
|
||||
+7
-9
@@ -27,19 +27,17 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Schema\rebuildActions;
|
||||
namespace Espo\Core\Rebuild\Actions;
|
||||
|
||||
use Espo\Core\Rebuild\RebuildAction;
|
||||
use Espo\Core\Utils\Currency\DatabasePopulator;
|
||||
|
||||
class Currency extends \Espo\Core\Utils\Database\Schema\BaseRebuildActions
|
||||
class CurrencyRates implements RebuildAction
|
||||
{
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function afterRebuild()
|
||||
{
|
||||
$populator = new DatabasePopulator($this->getConfig(), $this->getEntityManager());
|
||||
public function __construct(private DatabasePopulator $databasePopulator) {}
|
||||
|
||||
$populator->process();
|
||||
public function process(): void
|
||||
{
|
||||
$this->databasePopulator->process();
|
||||
}
|
||||
}
|
||||
@@ -368,11 +368,11 @@ abstract class Base
|
||||
//check database version
|
||||
if (isset($manifest['database'])) {
|
||||
$databaseHelper = $this->getDatabaseHelper();
|
||||
$databaseType = $databaseHelper->getDatabaseType();
|
||||
$databaseType = $databaseHelper->getType();
|
||||
$databaseTypeLc = strtolower($databaseType);
|
||||
|
||||
if (isset($manifest['database'][$databaseTypeLc])) {
|
||||
$databaseVersion = $databaseHelper->getDatabaseVersion();
|
||||
$databaseVersion = $databaseHelper->getVersion();
|
||||
|
||||
if ($databaseVersion) {
|
||||
$res &= $this->checkVersions(
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Core\Utils\Currency;
|
||||
|
||||
use Espo\Entities\Currency;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Core\Utils\Config;
|
||||
|
||||
@@ -37,20 +38,14 @@ use Espo\Core\Utils\Config;
|
||||
*/
|
||||
class DatabasePopulator
|
||||
{
|
||||
private Config $config;
|
||||
|
||||
private EntityManager $entityManager;
|
||||
|
||||
public function __construct(Config $config, EntityManager $entityManager)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private EntityManager $entityManager)
|
||||
{}
|
||||
|
||||
public function process(): void
|
||||
{
|
||||
$defaultCurrency = $this->config->get('defaultCurrency');
|
||||
|
||||
$baseCurrency = $this->config->get('baseCurrency');
|
||||
$currencyRates = $this->config->get('currencyRates');
|
||||
|
||||
@@ -62,13 +57,13 @@ class DatabasePopulator
|
||||
|
||||
$delete = $this->entityManager->getQueryBuilder()
|
||||
->delete()
|
||||
->from('Currency')
|
||||
->from(Currency::ENTITY_TYPE)
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($delete);
|
||||
|
||||
foreach ($currencyRates as $currencyName => $rate) {
|
||||
$this->entityManager->createEntity('Currency', [
|
||||
$this->entityManager->createEntity(Currency::ENTITY_TYPE, [
|
||||
'id' => $currencyName,
|
||||
'rate' => $rate,
|
||||
]);
|
||||
@@ -76,8 +71,8 @@ class DatabasePopulator
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,float> $currencyRates
|
||||
* @return array<string,float>
|
||||
* @param array<string, float> $currencyRates
|
||||
* @return array<string, float>
|
||||
*/
|
||||
private function exchangeRates(string $baseCurrency, string $defaultCurrency, array $currencyRates): array
|
||||
{
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database;
|
||||
|
||||
use Espo\Core\Utils\Config;
|
||||
|
||||
class ConfigDataProvider
|
||||
{
|
||||
private const DEFAULT_PLATFORM = 'Mysql';
|
||||
|
||||
public function __construct(private Config $config) {}
|
||||
|
||||
public function getPlatform(): string
|
||||
{
|
||||
return $this->config->get('database.platform') ?? self::DEFAULT_PLATFORM;
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,8 @@ class ConnectionFactoryFactory
|
||||
public function create(string $platform, PDO $pdo): ConnectionFactory
|
||||
{
|
||||
/** @var ?class-string<ConnectionFactory> $className */
|
||||
$className = $this->metadata->get(['app', 'database', 'dbalConnectionFactoryClassNameMap', $platform]);
|
||||
$className = $this->metadata
|
||||
->get(['app', 'database', 'platforms', $platform, 'dbalConnectionFactoryClassName']);
|
||||
|
||||
if (!$className) {
|
||||
throw new RuntimeException("No DBAL ConnectionFactory for {$platform}.");
|
||||
|
||||
+7
-12
@@ -29,18 +29,13 @@
|
||||
|
||||
namespace Espo\Core\Utils\Database;
|
||||
|
||||
/**
|
||||
* ORM converter wrapper.
|
||||
*/
|
||||
class Converter
|
||||
interface DetailsProvider
|
||||
{
|
||||
public function __construct(private Orm\Converter $ormConverter) {}
|
||||
public function getType(): string;
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public function process()
|
||||
{
|
||||
return $this->ormConverter->process();
|
||||
}
|
||||
public function getVersion(): string;
|
||||
|
||||
public function getServerVersion(): string;
|
||||
|
||||
public function getParam(string $name): ?string;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database;
|
||||
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use PDO;
|
||||
use RuntimeException;
|
||||
|
||||
class DetailsProviderFactory
|
||||
{
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
private Metadata $metadata
|
||||
) {}
|
||||
|
||||
public function create(string $platform, PDO $pdo): DetailsProvider
|
||||
{
|
||||
/** @var ?class-string<DetailsProvider> $className */
|
||||
$className = $this->metadata
|
||||
->get(['app', 'database', 'platforms', $platform, 'detailsProviderClassName']);
|
||||
|
||||
if (!$className) {
|
||||
throw new RuntimeException("No Details-Provider for {$platform}.");
|
||||
}
|
||||
|
||||
$binding = BindingContainerBuilder::create()
|
||||
->bindInstance(PDO::class, $pdo)
|
||||
->build();
|
||||
|
||||
return $this->injectableFactory->createWithBinding($className, $binding);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\DetailsProviders;
|
||||
|
||||
use Espo\Core\Utils\Database\DetailsProvider;
|
||||
use PDO;
|
||||
|
||||
class MysqlDetailsProvider implements DetailsProvider
|
||||
{
|
||||
public const TYPE_MYSQL = 'MySQL';
|
||||
public const TYPE_MARIADB = 'MariaDB';
|
||||
|
||||
public function __construct(
|
||||
private PDO $pdo
|
||||
) {}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
$version = $this->getFullDatabaseVersion() ?? '';
|
||||
|
||||
if (preg_match('/mariadb/i', $version)) {
|
||||
return self::TYPE_MARIADB;
|
||||
}
|
||||
|
||||
return self::TYPE_MYSQL;
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
$fullVersion = $this->getFullDatabaseVersion() ?? '';
|
||||
|
||||
if (preg_match('/[0-9]+\.[0-9]+\.[0-9]+/', $fullVersion, $match)) {
|
||||
return $match[0];
|
||||
}
|
||||
|
||||
return '0.0.0';
|
||||
}
|
||||
|
||||
public function getServerVersion(): string
|
||||
{
|
||||
return (string) $this->getParam('version');
|
||||
}
|
||||
|
||||
public function getParam(string $name): ?string
|
||||
{
|
||||
$sql = "SHOW VARIABLES LIKE :param";;
|
||||
|
||||
$sth = $this->pdo->prepare($sql);
|
||||
$sth->execute([':param' => $name]);
|
||||
|
||||
$row = $sth->fetch(PDO::FETCH_NUM);
|
||||
|
||||
$index = 1;
|
||||
|
||||
$value = $row[$index] ?: null;
|
||||
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
private function getFullDatabaseVersion(): ?string
|
||||
{
|
||||
$sql = "select version()";
|
||||
|
||||
$sth = $this->pdo->prepare($sql);
|
||||
$sth->execute();
|
||||
|
||||
/** @var string|null|false $result */
|
||||
$result = $sth->fetchColumn();
|
||||
|
||||
if ($result === false || $result === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -29,36 +29,30 @@
|
||||
|
||||
namespace Espo\Core\Utils\Database;
|
||||
|
||||
use Doctrine\DBAL\Connection as DbalConnection;
|
||||
|
||||
use Espo\Core\ORM\DatabaseParamsFactory;
|
||||
use Espo\Core\ORM\PDO\PDOFactoryFactory;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Database\DBAL\ConnectionFactoryFactory as DBALConnectionFactoryFactory;
|
||||
use Espo\ORM\DatabaseParams;
|
||||
|
||||
use Doctrine\DBAL\Connection as DBALConnection;
|
||||
|
||||
use PDO;
|
||||
use RuntimeException;
|
||||
|
||||
class Helper
|
||||
{
|
||||
public const TYPE_MYSQL = 'MySQL';
|
||||
public const TYPE_MARIADB = 'MariaDB';
|
||||
public const TYPE_POSTGRESQL = 'PostgreSQL';
|
||||
|
||||
private const DEFAULT_PLATFORM = 'Mysql';
|
||||
|
||||
private const DEFAULT_INDEX_LIMIT = 1000;
|
||||
|
||||
private ?DBALConnection $dbalConnection = null;
|
||||
private ?PDO $pdoConnection = null;
|
||||
private ?DbalConnection $dbalConnection = null;
|
||||
private ?PDO $pdo = null;
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private PDOFactoryFactory $pdoFactoryFactory,
|
||||
private DBALConnectionFactoryFactory $dbalConnectionFactoryFactory
|
||||
private DBALConnectionFactoryFactory $dbalConnectionFactoryFactory,
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
private DetailsProviderFactory $detailsProviderFactory,
|
||||
private DatabaseParamsFactory $databaseParamsFactory
|
||||
) {}
|
||||
|
||||
public function getDbalConnection(): DBALConnection
|
||||
public function getDbalConnection(): DbalConnection
|
||||
{
|
||||
if (!isset($this->dbalConnection)) {
|
||||
$this->dbalConnection = $this->createDbalConnection();
|
||||
@@ -67,251 +61,94 @@ class Helper
|
||||
return $this->dbalConnection;
|
||||
}
|
||||
|
||||
public function getPdoConnection(): PDO
|
||||
public function getPDO(): PDO
|
||||
{
|
||||
if (!isset($this->pdoConnection)) {
|
||||
$this->pdoConnection = $this->createPdoConnection();
|
||||
if (!isset($this->pdo)) {
|
||||
$this->pdo = $this->createPDO();
|
||||
}
|
||||
|
||||
return $this->pdoConnection;
|
||||
return $this->pdo;
|
||||
}
|
||||
|
||||
public function setPdoConnection(PDO $pdoConnection): void
|
||||
/**
|
||||
* Clone with another PDO connection.
|
||||
*/
|
||||
public function withPDO(PDO $pdo): self
|
||||
{
|
||||
$this->pdoConnection = $pdoConnection;
|
||||
$obj = clone $this;
|
||||
$obj->pdo = $pdo;
|
||||
$obj->dbalConnection = null;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function createDbalConnection(): DBALConnection
|
||||
/**
|
||||
* Create a PDO connection.
|
||||
*/
|
||||
public function createPDO(?DatabaseParams $params = null, bool $skipDatabaseName = false): PDO
|
||||
{
|
||||
/** @var ?array<string, mixed> $params */
|
||||
$params = $this->config->get('database');
|
||||
$params = $params ?? $this->databaseParamsFactory->create();
|
||||
|
||||
if (empty($params)) {
|
||||
throw new RuntimeException('Database params cannot be empty for DBAL connection.');
|
||||
if ($skipDatabaseName) {
|
||||
$params = $params->withName(null);
|
||||
}
|
||||
|
||||
$databaseParams = $this->createDatabaseParams($params);
|
||||
return $this->pdoFactoryFactory
|
||||
->create($params->getPlatform() ?? '')
|
||||
->create($params);
|
||||
}
|
||||
|
||||
$platform = $databaseParams->getPlatform() ?? self::DEFAULT_PLATFORM;
|
||||
private function createDbalConnection(): DbalConnection
|
||||
{
|
||||
$params = $this->databaseParamsFactory->create();
|
||||
|
||||
$platform = $params->getPlatform();
|
||||
|
||||
if (!$platform) {
|
||||
throw new RuntimeException("No database platform.");
|
||||
}
|
||||
|
||||
return $this->dbalConnectionFactoryFactory
|
||||
->create($platform, $this->getPdoConnection())
|
||||
->create($databaseParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create PDO connection.
|
||||
*
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
public function createPdoConnection(array $params = [], bool $skipDatabaseName = false): PDO
|
||||
{
|
||||
$params = array_merge(
|
||||
$this->config->get('database') ?? [],
|
||||
$params
|
||||
);
|
||||
|
||||
if ($skipDatabaseName && isset($params['dbname'])) {
|
||||
unset($params['dbname']);
|
||||
}
|
||||
|
||||
$databaseParams = $this->createDatabaseParams($params);
|
||||
|
||||
$platform = $databaseParams->getPlatform();
|
||||
|
||||
$pdoFactory = $this->pdoFactoryFactory->create($platform ?? '');
|
||||
|
||||
return $pdoFactory->create($databaseParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
private function createDatabaseParams(array $params): DatabaseParams
|
||||
{
|
||||
$databaseParams = DatabaseParams::create()
|
||||
->withHost($params['host'] ?? null)
|
||||
->withPort(isset($params['port']) ? (int) $params['port'] : null)
|
||||
->withName($params['dbname'] ?? null)
|
||||
->withUsername($params['user'] ?? null)
|
||||
->withPassword($params['password'] ?? null)
|
||||
->withCharset($params['charset'] ?? 'utf8')
|
||||
->withPlatform($params['platform'] ?? null)
|
||||
->withSslCa($params['sslCA'] ?? null)
|
||||
->withSslCert($params['sslCert'] ?? null)
|
||||
->withSslKey($params['sslKey'] ?? null)
|
||||
->withSslCaPath($params['sslCAPath'] ?? null)
|
||||
->withSslCipher($params['sslCipher'] ?? null)
|
||||
->withSslVerifyDisabled($params['sslVerifyDisabled'] ?? false);
|
||||
|
||||
if (!$databaseParams->getPlatform()) {
|
||||
$databaseParams = $databaseParams->withPlatform(self::DEFAULT_PLATFORM);
|
||||
}
|
||||
|
||||
return $databaseParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximum index length. If a table name is null, then get a value for all database tables.
|
||||
*/
|
||||
public function getMaxIndexLength(?string $tableName = null): int
|
||||
{
|
||||
$databaseType = $this->getDatabaseType();
|
||||
|
||||
if ($databaseType === self::TYPE_POSTGRESQL) {
|
||||
return 2704; // @todo Revise.
|
||||
}
|
||||
|
||||
$tableEngine = $this->getTableEngine($tableName);
|
||||
|
||||
if (!$tableEngine) {
|
||||
return self::DEFAULT_INDEX_LIMIT;
|
||||
}
|
||||
|
||||
switch ($tableEngine) {
|
||||
case 'InnoDB':
|
||||
$databaseType = $this->getDatabaseType();
|
||||
$version = $this->getDatabaseVersion() ?? '';
|
||||
|
||||
switch ($databaseType) {
|
||||
case self::TYPE_MARIADB:
|
||||
if (version_compare($version, '10.2.2') >= 0) {
|
||||
return 3072; // InnoDB, MariaDB 10.2.2+
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case self::TYPE_MYSQL:
|
||||
return 3072;
|
||||
}
|
||||
|
||||
return 767; // InnoDB
|
||||
}
|
||||
|
||||
return 1000; // MyISAM
|
||||
->create($platform, $this->getPDO())
|
||||
->create($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a database type (MySQL, MariaDB, PostgreSQL).
|
||||
*/
|
||||
public function getDatabaseType(): string
|
||||
public function getType(): string
|
||||
{
|
||||
$version = $this->getFullDatabaseVersion() ?? '';
|
||||
|
||||
if (preg_match('/mariadb/i', $version)) {
|
||||
return self::TYPE_MARIADB;
|
||||
}
|
||||
|
||||
if (preg_match('/postgresql/i', $version)) {
|
||||
return self::TYPE_POSTGRESQL;
|
||||
}
|
||||
|
||||
return self::TYPE_MYSQL;
|
||||
}
|
||||
|
||||
private function getFullDatabaseVersion(): ?string
|
||||
{
|
||||
$connection = $this->getPdoConnection();
|
||||
|
||||
$sth = $connection->prepare("select version()");
|
||||
|
||||
$sth->execute();
|
||||
|
||||
/** @var string|null|false $result */
|
||||
$result = $sth->fetchColumn();
|
||||
|
||||
if ($result === false || $result === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
return $this->createDetailsProvider()->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a database version.
|
||||
*
|
||||
* @todo Add PostgreSQL support.
|
||||
*/
|
||||
public function getDatabaseVersion(): ?string
|
||||
public function getVersion(): string
|
||||
{
|
||||
$fullVersion = $this->getFullDatabaseVersion() ?? '';
|
||||
|
||||
if (preg_match('/[0-9]+\.[0-9]+\.[0-9]+/', $fullVersion, $match)) {
|
||||
return $match[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
return $this->createDetailsProvider()->getVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a table or default engine. If a table name is null, get a value for all database tables.
|
||||
* Get a database parameter.
|
||||
*/
|
||||
private function getTableEngine(?string $tableName = null): ?string
|
||||
public function getParam(string $name): ?string
|
||||
{
|
||||
$databaseType = $this->getDatabaseType();
|
||||
|
||||
if (!in_array($databaseType, [self::TYPE_MYSQL, self::TYPE_MARIADB])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$query = $tableName ?
|
||||
"SHOW TABLE STATUS WHERE Engine = 'MyISAM' AND Name = :tableName" :
|
||||
"SHOW TABLE STATUS WHERE Engine = 'MyISAM'";
|
||||
|
||||
$vars = [];
|
||||
|
||||
if ($tableName) {
|
||||
$vars[':tableName'] = $tableName;
|
||||
}
|
||||
|
||||
$sth = $this->getPdoConnection()->prepare($query);
|
||||
$sth->execute($vars);
|
||||
|
||||
$result = $sth->fetchColumn();
|
||||
|
||||
if (!empty($result)) {
|
||||
return 'MyISAM';
|
||||
}
|
||||
|
||||
return 'InnoDB';
|
||||
return $this->createDetailsProvider()->getParam($name);
|
||||
}
|
||||
|
||||
public function getDatabaseParam(string $name): ?string
|
||||
/**
|
||||
* Get a database server version string.
|
||||
*/
|
||||
public function getServerVersion(): string
|
||||
{
|
||||
$databaseType = $this->getDatabaseType();
|
||||
|
||||
if ($databaseType === self::TYPE_POSTGRESQL) {
|
||||
// @todo Implement.
|
||||
return null;
|
||||
}
|
||||
|
||||
$sql = "SHOW VARIABLES LIKE :param";;
|
||||
|
||||
$sth = $this->getPdoConnection()->prepare($sql);
|
||||
$sth->execute([':param' => $name]);
|
||||
|
||||
$row = $sth->fetch(PDO::FETCH_NUM);
|
||||
|
||||
$index = 1;
|
||||
|
||||
$value = $row[$index] ?: null;
|
||||
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
return $this->createDetailsProvider()->getServerVersion();
|
||||
}
|
||||
|
||||
public function getDatabaseServerVersion(): string
|
||||
private function createDetailsProvider(): DetailsProvider
|
||||
{
|
||||
$databaseType = $this->getDatabaseType();
|
||||
$platform = $this->configDataProvider->getPlatform();
|
||||
|
||||
$param = $databaseType === self::TYPE_POSTGRESQL ?
|
||||
'server_version' :
|
||||
'version';
|
||||
|
||||
return (string) $this->getDatabaseParam($param);
|
||||
return $this->detailsProviderFactory->create($platform, $this->getPDO());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,13 @@
|
||||
|
||||
namespace Espo\Core\Utils\Database\Orm;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Espo\Core\Utils\Database\ConfigDataProvider;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\ORM\Defs\AttributeDefs;
|
||||
use Espo\ORM\Defs\IndexDefs;
|
||||
use Espo\ORM\Defs\RelationDefs;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\Utils\Database\Schema\Utils as SchemaUtils;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Metadata\Helper as MetadataHelper;
|
||||
@@ -42,7 +45,10 @@ class Converter
|
||||
/** @var ?array<string, mixed> */
|
||||
private $entityDefs = null;
|
||||
|
||||
private string $defaultFieldType = 'varchar';
|
||||
private string $defaultAttributeType = Entity::VARCHAR;
|
||||
|
||||
private const INDEX_TYPE_UNIQUE = 'unique';
|
||||
private const INDEX_TYPE_INDEX = 'index';
|
||||
|
||||
/** @var array<string, int> */
|
||||
private $defaultLength = [
|
||||
@@ -50,7 +56,7 @@ class Converter
|
||||
'int' => 11,
|
||||
];
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
/** @var array<string, mixed> */
|
||||
private $defaultValue = [
|
||||
'bool' => false,
|
||||
];
|
||||
@@ -60,7 +66,7 @@ class Converter
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $fieldAccordances = [
|
||||
private $paramMap = [
|
||||
'type' => 'type',
|
||||
'dbType' => 'dbType',
|
||||
'maxLength' => 'len',
|
||||
@@ -88,7 +94,7 @@ class Converter
|
||||
/** @var array<string, mixed> */
|
||||
private $idParams = [
|
||||
'dbType' => 'varchar',
|
||||
'len' => 24,
|
||||
'len' => 24, // @todo Make configurable.
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -101,12 +107,20 @@ class Converter
|
||||
'additionalTables',
|
||||
];
|
||||
|
||||
private IndexHelper $indexHelper;
|
||||
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private Config $config,
|
||||
private RelationManager $relationManager,
|
||||
private MetadataHelper $metadataHelper
|
||||
) {}
|
||||
private RelationConverter $relationConverter,
|
||||
private MetadataHelper $metadataHelper,
|
||||
ConfigDataProvider $configDataProvider,
|
||||
IndexHelperFactory $indexHelperFactory
|
||||
) {
|
||||
$platform = $configDataProvider->getPlatform();
|
||||
|
||||
$this->indexHelper = $indexHelperFactory->create($platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $reload
|
||||
@@ -137,29 +151,37 @@ class Converter
|
||||
$ormMetadata[$entityType]['skipRebuild'] = true;
|
||||
}
|
||||
|
||||
/** @var array<string,array<string,mixed>> $ormMetadata */
|
||||
$ormMetadata = Util::merge($ormMetadata, $this->convertEntity($entityType, $entityMetadata));
|
||||
/** @var array<string, array<string, mixed>> $ormMetadata */
|
||||
$ormMetadata = Util::merge(
|
||||
$ormMetadata,
|
||||
$this->convertEntity($entityType, $entityMetadata)
|
||||
);
|
||||
}
|
||||
|
||||
$ormMetadata = $this->afterFieldsProcess($ormMetadata);
|
||||
|
||||
foreach ($ormMetadata as $entityType => $entityOrmMetadata) {
|
||||
/** @var array<string,array<string,mixed>> $ormMetadata */
|
||||
foreach ($ormMetadata as $entityOrmMetadata) {
|
||||
/** @var array<string, array<string, mixed>> $ormMetadata */
|
||||
$ormMetadata = Util::merge(
|
||||
$ormMetadata,
|
||||
$this->createRelationsEntityDefs($entityType, $entityOrmMetadata)
|
||||
$this->createEntityTypesFromRelations($entityOrmMetadata)
|
||||
);
|
||||
|
||||
/** @var array<string,array<string,mixed>> $ormMetadata */
|
||||
/** @var array<string, array<string, mixed>> $ormMetadata */
|
||||
$ormMetadata = Util::merge(
|
||||
$ormMetadata,
|
||||
$this->createAdditionalEntityTypes($entityType, $entityOrmMetadata)
|
||||
$this->createAdditionalEntityTypes($entityOrmMetadata)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->afterProcess($ormMetadata);
|
||||
}
|
||||
|
||||
private function composeIndexKey(IndexDefs $defs, string $entityType): string
|
||||
{
|
||||
return $this->indexHelper->composeKey($defs, $entityType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $entityMetadata
|
||||
* @return array<string, mixed>
|
||||
@@ -268,7 +290,7 @@ class Converter
|
||||
$constName = strtoupper(Util::toUnderScore($attributeParams['type']));
|
||||
|
||||
if (!defined('Espo\\ORM\\Entity::' . $constName)) {
|
||||
$attributeParams['type'] = $this->defaultFieldType;
|
||||
$attributeParams['type'] = $this->defaultAttributeType;
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -473,36 +495,37 @@ class Converter
|
||||
}
|
||||
}
|
||||
|
||||
// @todo move to separate file
|
||||
$scopeDefs = $this->metadata->get('scopes.'.$entityType);
|
||||
// @todo Refactor.
|
||||
/** @var array<string, mixed> $scopeDefs */
|
||||
$scopeDefs = $this->metadata->get(['scopes', $entityType]) ?? [];
|
||||
|
||||
if (isset($scopeDefs['stream']) && $scopeDefs['stream']) {
|
||||
if ($scopeDefs['stream'] ?? false) {
|
||||
if (!isset($entityMetadata['fields']['isFollowed'])) {
|
||||
$ormMetadata[$entityType]['fields']['isFollowed'] = [
|
||||
'type' => 'varchar',
|
||||
'type' => Entity::VARCHAR,
|
||||
'notStorable' => true,
|
||||
'notExportable' => true,
|
||||
];
|
||||
|
||||
$ormMetadata[$entityType]['fields']['followersIds'] = [
|
||||
'type' => 'jsonArray',
|
||||
'type' => Entity::JSON_ARRAY,
|
||||
'notStorable' => true,
|
||||
'notExportable' => true,
|
||||
];
|
||||
|
||||
$ormMetadata[$entityType]['fields']['followersNames'] = [
|
||||
'type' => 'jsonObject',
|
||||
'type' => Entity::JSON_OBJECT,
|
||||
'notStorable' => true,
|
||||
'notExportable' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// @todo move to separate file
|
||||
// @todo Refactor.
|
||||
if ($this->metadata->get(['entityDefs', $entityType, 'optimisticConcurrencyControl'])) {
|
||||
$ormMetadata[$entityType]['fields']['versionNumber'] = [
|
||||
'type' => Entity::INT,
|
||||
'dbType' => 'bigint',
|
||||
'dbType' => Types::BIGINT,
|
||||
'notExportable' => true,
|
||||
];
|
||||
}
|
||||
@@ -511,9 +534,9 @@ class Converter
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $fieldParams
|
||||
* @param ?array<string,mixed> $fieldTypeMetadata
|
||||
* @return array<string,mixed>|false
|
||||
* @param array<string, mixed> $fieldParams
|
||||
* @param ?array<string, mixed> $fieldTypeMetadata
|
||||
* @return array<string, mixed>|false
|
||||
*/
|
||||
private function convertField(
|
||||
string $entityType,
|
||||
@@ -528,7 +551,7 @@ class Converter
|
||||
$this->prepareFieldParamsBeforeConvert($fieldParams);
|
||||
|
||||
if (isset($fieldTypeMetadata['fieldDefs'])) {
|
||||
/** @var array<string,mixed> $fieldParams */
|
||||
/** @var array<string, mixed> $fieldParams */
|
||||
$fieldParams = Util::merge($fieldParams, $fieldTypeMetadata['fieldDefs']);
|
||||
}
|
||||
|
||||
@@ -564,7 +587,7 @@ class Converter
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $fieldParams
|
||||
* @param array<string, mixed> $fieldParams
|
||||
*/
|
||||
private function prepareFieldParamsBeforeConvert(array &$fieldParams): void
|
||||
{
|
||||
@@ -589,16 +612,16 @@ class Converter
|
||||
}
|
||||
|
||||
$relationships = [];
|
||||
foreach ($entityMetadata['links'] as $linkName => $linkParams) {
|
||||
|
||||
foreach ($entityMetadata['links'] as $linkName => $linkParams) {
|
||||
if (isset($linkParams['skipOrmDefs']) && $linkParams['skipOrmDefs'] === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$convertedLink = $this->relationManager->convert($linkName, $linkParams, $entityType, $ormMetadata);
|
||||
$convertedLink = $this->relationConverter->process($linkName, $linkParams, $entityType, $ormMetadata);
|
||||
|
||||
if (isset($convertedLink)) {
|
||||
/** @var array<string,mixed> $relationships */
|
||||
if ($convertedLink) {
|
||||
/** @var array<string, mixed> $relationships */
|
||||
$relationships = Util::merge($convertedLink, $relationships);
|
||||
}
|
||||
}
|
||||
@@ -614,7 +637,7 @@ class Converter
|
||||
{
|
||||
$values = [];
|
||||
|
||||
foreach ($this->fieldAccordances as $espoType => $ormType) {
|
||||
foreach ($this->paramMap as $espoType => $ormType) {
|
||||
if (!array_key_exists($espoType, $attributeParams)) {
|
||||
continue;
|
||||
}
|
||||
@@ -712,38 +735,85 @@ class Converter
|
||||
/**
|
||||
* @param array<string, mixed> $ormMetadata
|
||||
*/
|
||||
private function applyIndexes(&$ormMetadata, string $entityType): void
|
||||
private function applyIndexes(array &$ormMetadata, string $entityType): void
|
||||
{
|
||||
if (isset($ormMetadata[$entityType]['fields'])) {
|
||||
$indexList = SchemaUtils::getEntityIndexListByFieldsDefs($ormMetadata[$entityType]['fields']);
|
||||
$defs = &$ormMetadata[$entityType];
|
||||
|
||||
$defs['indexes'] ??= [];
|
||||
|
||||
if (isset($defs['fields'])) {
|
||||
$indexList = self::getEntityIndexListFromAttributes($defs['fields']);
|
||||
|
||||
foreach ($indexList as $indexName => $indexParams) {
|
||||
if (!isset($ormMetadata[$entityType]['indexes'][$indexName])) {
|
||||
$ormMetadata[$entityType]['indexes'][$indexName] = $indexParams;
|
||||
if (!isset($defs['indexes'][$indexName])) {
|
||||
$defs['indexes'][$indexName] = $indexParams;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($ormMetadata[$entityType]['indexes'])) {
|
||||
foreach ($ormMetadata[$entityType]['indexes'] as $indexName => &$indexData) {
|
||||
$indexDefs = IndexDefs::fromRaw($indexData, $indexName);
|
||||
foreach ($defs['indexes'] as $indexName => &$indexData) {
|
||||
$indexDefs = IndexDefs::fromRaw($indexData, $indexName);
|
||||
|
||||
if (!$indexDefs->getKey()) {
|
||||
$indexData['key'] = SchemaUtils::generateIndexName($indexDefs, $entityType);
|
||||
}
|
||||
if (!$indexDefs->getKey()) {
|
||||
$indexData['key'] = $this->composeIndexKey($indexDefs, $entityType);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($ormMetadata[$entityType]['relations'])) {
|
||||
foreach ($ormMetadata[$entityType]['relations'] as &$relationData) {
|
||||
if (isset($relationData['indexes'])) {
|
||||
foreach ($relationData['indexes'] as $indexName => &$indexData) {
|
||||
$indexDefs = IndexDefs::fromRaw($indexData, $indexName);
|
||||
if (isset($defs['relations'])) {
|
||||
foreach ($defs['relations'] as &$relationData) {
|
||||
$type = $relationData['type'] ?? null;
|
||||
|
||||
$relationName = $relationData['relationName'] ?? '';
|
||||
if ($type !== Entity::MANY_MANY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$indexData['key'] = SchemaUtils::generateIndexName($indexDefs, $relationName);
|
||||
$relationName = $relationData['relationName'] ?? '';
|
||||
|
||||
$relationData['indexes'] ??= [];
|
||||
|
||||
$uniqueColumnList = [];
|
||||
|
||||
foreach (($relationData['midKeys'] ?? []) as $midKey) {
|
||||
$indexName = $midKey;
|
||||
|
||||
$indexDefs = IndexDefs::fromRaw(['columns' => [$midKey]], $indexName);
|
||||
|
||||
$relationData['indexes'][$indexName] = [
|
||||
'columns' => $indexDefs->getColumnList(),
|
||||
'key' => $this->composeIndexKey($indexDefs, ucfirst($relationName)),
|
||||
];
|
||||
|
||||
$uniqueColumnList[] = $midKey;
|
||||
}
|
||||
|
||||
foreach ($relationData['indexes'] as $indexName => &$indexData) {
|
||||
if (!empty($indexData['key'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$indexDefs = IndexDefs::fromRaw($indexData, $indexName);
|
||||
|
||||
$indexData['key'] = $this->composeIndexKey($indexDefs, ucfirst($relationName));
|
||||
}
|
||||
|
||||
foreach (($relationData['conditions'] ?? []) as $column => $fieldParams) {
|
||||
$uniqueColumnList[] = $column;
|
||||
}
|
||||
|
||||
if ($uniqueColumnList !== []) {
|
||||
$indexName = implode('_', $uniqueColumnList);
|
||||
|
||||
$indexDefs = IndexDefs
|
||||
::fromRaw([
|
||||
'columns' => $uniqueColumnList,
|
||||
'type' => self::INDEX_TYPE_UNIQUE,
|
||||
], $indexName);
|
||||
|
||||
$relationData['indexes'][$indexName] = [
|
||||
'type' => self::INDEX_TYPE_UNIQUE,
|
||||
'columns' => $indexDefs->getColumnList(),
|
||||
'key' => $this->composeIndexKey($indexDefs, ucfirst($relationName)),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -753,54 +823,90 @@ class Converter
|
||||
* @param array<string, mixed> $defs
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function createAdditionalEntityTypes(string $entityType, array $defs): array
|
||||
private function createAdditionalEntityTypes(array $defs): array
|
||||
{
|
||||
if (empty($defs['additionalTables'])) {
|
||||
/** @var array<string, array<string, mixed>> $additionalDefs */
|
||||
$additionalDefs = $defs['additionalTables'] ?? [];
|
||||
|
||||
if ($additionalDefs === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $defs['additionalTables'];
|
||||
/** @var string[] $entityTypeList */
|
||||
$entityTypeList = array_keys($additionalDefs);
|
||||
|
||||
foreach ($entityTypeList as $itemEntityType) {
|
||||
$this->applyIndexes($additionalDefs, $itemEntityType);
|
||||
}
|
||||
|
||||
return $additionalDefs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $defs
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function createRelationsEntityDefs(string $entityType, array $defs): array
|
||||
private function createEntityTypesFromRelations(array $defs): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($defs['relations'] as $relationParams) {
|
||||
if ($relationParams['type'] !== 'manyMany') {
|
||||
foreach ($defs['relations'] as $name => $relationParams) {
|
||||
$relationDefs = RelationDefs::fromRaw($relationParams, $name);
|
||||
|
||||
if ($relationDefs->getType() !== Entity::MANY_MANY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relationEntityType = ucfirst($relationParams['relationName']);
|
||||
$relationEntityType = ucfirst($relationDefs->getRelationshipName());
|
||||
|
||||
$itemDefs = [
|
||||
'skipRebuild' => true,
|
||||
'fields' => [
|
||||
'id' => [
|
||||
'type' => 'id',
|
||||
'type' => Entity::ID,
|
||||
'autoincrement' => true,
|
||||
'dbType' => 'bigint', // ignored because of `skipRebuild`
|
||||
'dbType' => Types::BIGINT, // ignored because of `skipRebuild`
|
||||
],
|
||||
'deleted' => [
|
||||
'type' => 'bool'
|
||||
'type' => Entity::BOOL,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($relationParams['midKeys'] ?? [] as $key) {
|
||||
$key1 = $relationDefs->getMidKey();
|
||||
$key2 = $relationDefs->getForeignMidKey();
|
||||
|
||||
$midKeys = [$key1, $key2];
|
||||
|
||||
foreach ($midKeys as $key) {
|
||||
$itemDefs['fields'][$key] = [
|
||||
'type' => 'foreignId',
|
||||
'type' => Entity::FOREIGN_ID,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($relationParams['additionalColumns'] ?? [] as $columnName => $columnItem) {
|
||||
$itemDefs['fields'][$columnName] = [
|
||||
'type' => $columnItem['type'] ?? 'varchar',
|
||||
foreach ($relationDefs->getParam('additionalColumns') ?? [] as $columnName => $columnItem) {
|
||||
$columnItem['type'] ??= Entity::VARCHAR;
|
||||
|
||||
$attributeDefs = AttributeDefs::fromRaw($columnItem, $columnName);
|
||||
|
||||
$columnDefs = [
|
||||
'type' => $attributeDefs->getType(),
|
||||
];
|
||||
|
||||
if ($attributeDefs->getLength()) {
|
||||
$columnDefs['len'] = $attributeDefs->getLength();
|
||||
}
|
||||
|
||||
if ($attributeDefs->getParam('default') !== null) {
|
||||
$columnDefs['default'] = $attributeDefs->getParam('default');
|
||||
}
|
||||
|
||||
$itemDefs['fields'][$columnName] = $columnDefs;
|
||||
}
|
||||
|
||||
foreach ($relationDefs->getIndexList() as $indexDefs) {
|
||||
$itemDefs['indexes'] ??= [];
|
||||
$itemDefs['indexes'][] = self::convertIndexDefsToRaw($indexDefs);
|
||||
}
|
||||
|
||||
$result[$relationEntityType] = $itemDefs;
|
||||
@@ -808,4 +914,93 @@ class Converter
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function convertIndexDefsToRaw(IndexDefs $indexDefs): array
|
||||
{
|
||||
return [
|
||||
'type' => $indexDefs->isUnique() ? self::INDEX_TYPE_UNIQUE : self::INDEX_TYPE_INDEX,
|
||||
'columns' => $indexDefs->getColumnList(),
|
||||
'flags' => $indexDefs->getFlagList(),
|
||||
'key' => $indexDefs->getKey(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributesMetadata
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function getEntityIndexListFromAttributes(array $attributesMetadata): array
|
||||
{
|
||||
$indexList = [];
|
||||
|
||||
foreach ($attributesMetadata as $attributeName => $rawParams) {
|
||||
$attributeDefs = AttributeDefs::fromRaw($rawParams, $attributeName);
|
||||
|
||||
if ($attributeDefs->isNotStorable()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$indexType = self::getIndexTypeByAttributeDefs($attributeDefs);
|
||||
$indexName = self::getIndexNameByAttributeDefs($attributeDefs);
|
||||
|
||||
if (!$indexType || !$indexName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$keyValue = $attributeDefs->getParam($indexType);
|
||||
|
||||
if ($keyValue === true) {
|
||||
$indexList[$indexName]['type'] = $indexType;
|
||||
$indexList[$indexName]['columns'] = [$attributeName];
|
||||
}
|
||||
else if (is_string($keyValue)) {
|
||||
$indexList[$indexName]['type'] = $indexType;
|
||||
$indexList[$indexName]['columns'][] = $attributeName;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
return $indexList;
|
||||
}
|
||||
|
||||
|
||||
private static function getIndexTypeByAttributeDefs(AttributeDefs $attributeDefs): ?string
|
||||
{
|
||||
if (
|
||||
$attributeDefs->getType() !== Entity::ID &&
|
||||
$attributeDefs->getParam(self::INDEX_TYPE_UNIQUE)
|
||||
) {
|
||||
return self::INDEX_TYPE_UNIQUE;
|
||||
}
|
||||
|
||||
if ($attributeDefs->getParam(self::INDEX_TYPE_INDEX)) {
|
||||
return self::INDEX_TYPE_INDEX;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function getIndexNameByAttributeDefs(AttributeDefs $attributeDefs): ?string
|
||||
{
|
||||
$indexType = self::getIndexTypeByAttributeDefs($attributeDefs);
|
||||
|
||||
if (!$indexType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$keyValue = $attributeDefs->getParam($indexType);
|
||||
|
||||
if ($keyValue === true) {
|
||||
return $attributeDefs->getName();
|
||||
}
|
||||
|
||||
if (is_string($keyValue)) {
|
||||
return $keyValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-22
@@ -27,31 +27,14 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Schema\rebuildActions;
|
||||
namespace Espo\Core\Utils\Database\Orm;
|
||||
|
||||
use Espo\Core\Utils\Database\Schema\BaseRebuildActions as Base;
|
||||
use Espo\ORM\Defs\IndexDefs;
|
||||
|
||||
class AddSystemUser extends Base
|
||||
interface IndexHelper
|
||||
{
|
||||
/**
|
||||
* @return void
|
||||
* Compose an index DB name. Depending on database, the name can be unique, limited by a max length.
|
||||
*/
|
||||
public function afterRebuild()
|
||||
{
|
||||
$userId = $this->getConfig()->get('systemUserAttributes.id');
|
||||
|
||||
$user = $this->getEntityManager()->getEntity('User', $userId);
|
||||
|
||||
if ($user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$systemUserAttributes = $this->getConfig()->get('systemUserAttributes');
|
||||
|
||||
$user = $this->getEntityManager()->getNewEntity('User');
|
||||
|
||||
$user->set($systemUserAttributes);
|
||||
|
||||
$this->getEntityManager()->saveEntity($user);
|
||||
}
|
||||
public function composeKey(IndexDefs $defs, string $entityType): string;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Orm;
|
||||
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use RuntimeException;
|
||||
|
||||
class IndexHelperFactory
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
public function create(string $platform): IndexHelper
|
||||
{
|
||||
/** @var ?class-string<IndexHelper> $className */
|
||||
$className = $this->metadata
|
||||
->get(['app', 'database', 'platforms', $platform, 'indexHelperClassName']);
|
||||
|
||||
if (!$className) {
|
||||
throw new RuntimeException("No Index Helper for {$platform}");
|
||||
}
|
||||
|
||||
return $this->injectableFactory->create($className);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Orm\IndexHelpers;
|
||||
|
||||
use Espo\Core\Utils\Database\Orm\IndexHelper;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\ORM\Defs\IndexDefs;
|
||||
|
||||
class MysqlIndexHelper implements IndexHelper
|
||||
{
|
||||
private const MAX_LENGTH = 60;
|
||||
|
||||
public function composeKey(IndexDefs $defs, string $entityType): string
|
||||
{
|
||||
$name = $defs->getName();
|
||||
$prefix = $defs->isUnique() ? 'UNIQ' : 'IDX';
|
||||
|
||||
$parts = [$prefix, strtoupper(Util::toUnderScore($name))];
|
||||
|
||||
$key = implode('_', $parts);
|
||||
|
||||
return substr($key, 0, self::MAX_LENGTH);
|
||||
}
|
||||
}
|
||||
+11
-24
@@ -33,27 +33,17 @@ use Espo\Core\Utils\Util;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\Config;
|
||||
|
||||
class RelationManager
|
||||
class RelationConverter
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private Config $config
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param string $entityName
|
||||
* @param array<string, mixed> $linkParams
|
||||
* @return string
|
||||
*/
|
||||
public function getLinkEntityName($entityName, $linkParams)
|
||||
{
|
||||
return $linkParams['entity'] ?? $entityName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $relationName
|
||||
*/
|
||||
public function relationExists($relationName): bool
|
||||
private function relationExists($relationName): bool
|
||||
{
|
||||
if ($this->getRelationClass($relationName) !== false) {
|
||||
return true;
|
||||
@@ -85,12 +75,11 @@ class RelationManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Get foreign link.
|
||||
* Get a foreign link.
|
||||
*
|
||||
* @param array<string,mixed> $parentLinkParams
|
||||
* @param array<string,mixed> $currentEntityDefs
|
||||
*
|
||||
* @return array{name:string,params:array<string,mixed>}|false
|
||||
* @param array<string, mixed> $parentLinkParams
|
||||
* @param array<string, mixed> $currentEntityDefs
|
||||
* @return array{name: string, params: array<string, mixed>}|false
|
||||
*/
|
||||
private function getForeignLink($parentLinkParams, $currentEntityDefs)
|
||||
{
|
||||
@@ -107,16 +96,16 @@ class RelationManager
|
||||
/**
|
||||
* @param string $linkName
|
||||
* @param array<string, mixed> $linkParams
|
||||
* @param string $entityName
|
||||
* @param string $entityType
|
||||
* @param array<string, mixed> $ormMetadata
|
||||
* @return ?array<string, mixed>
|
||||
*/
|
||||
public function convert($linkName, $linkParams, $entityName, $ormMetadata)
|
||||
public function process(string $linkName, array $linkParams, string $entityType, array $ormMetadata): ?array
|
||||
{
|
||||
$entityDefs = $this->metadata->get('entityDefs');
|
||||
|
||||
$foreignEntityName = $this->getLinkEntityName($entityName, $linkParams);
|
||||
$foreignLink = $this->getForeignLink($linkParams, $entityDefs[$foreignEntityName]);
|
||||
$foreignEntityType = $linkParams['entity'] ?? $entityType;
|
||||
$foreignLink = $this->getForeignLink($linkParams, $entityDefs[$foreignEntityType]);
|
||||
|
||||
$currentType = $linkParams['type'];
|
||||
|
||||
@@ -132,7 +121,6 @@ class RelationManager
|
||||
$relType /*hasManyHasMany*/ :
|
||||
$currentType /*hasMany*/;
|
||||
|
||||
// relationDefs defined in separate file
|
||||
if (isset($linkParams['relationName'])) {
|
||||
$className = $this->getRelationClass($linkParams['relationName']);
|
||||
|
||||
@@ -150,9 +138,8 @@ class RelationManager
|
||||
|
||||
$helperClass = new $className($this->metadata, $ormMetadata, $entityDefs, $this->config);
|
||||
|
||||
return $helperClass->process($linkName, $entityName, $foreignLinkName, $foreignEntityName);
|
||||
return $helperClass->process($linkName, $entityType, $foreignLinkName, $foreignEntityType);
|
||||
}
|
||||
//END: relationDefs defined in separate file
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -30,16 +30,19 @@
|
||||
namespace Espo\Core\Utils\Database\Orm\Relations;
|
||||
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class Base extends \Espo\Core\Utils\Database\Orm\Base
|
||||
{
|
||||
private const DEFAULT_VARCHAR_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* @var array<string,mixed>
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $params;
|
||||
|
||||
/**
|
||||
* @var array<string,mixed>
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $foreignParams;
|
||||
|
||||
@@ -62,11 +65,11 @@ class Base extends \Espo\Core\Utils\Database\Orm\Base
|
||||
'additionalColumns',
|
||||
'midKeys',
|
||||
'noJoin',
|
||||
'indexes'
|
||||
'indexes',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function getParams()
|
||||
{
|
||||
@@ -74,7 +77,7 @@ class Base extends \Espo\Core\Utils\Database\Orm\Base
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function getForeignParams()
|
||||
{
|
||||
@@ -82,7 +85,7 @@ class Base extends \Espo\Core\Utils\Database\Orm\Base
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $params
|
||||
* @param array<string, mixed> $params
|
||||
* @return void
|
||||
*/
|
||||
protected function setParams(array $params)
|
||||
@@ -91,7 +94,7 @@ class Base extends \Espo\Core\Utils\Database\Orm\Base
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $foreignParams
|
||||
* @param array<string, mixed> $foreignParams
|
||||
* @return void
|
||||
*/
|
||||
protected function setForeignParams(array $foreignParams)
|
||||
@@ -134,7 +137,7 @@ class Base extends \Espo\Core\Utils\Database\Orm\Base
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function getForeignLinkParams()
|
||||
{
|
||||
@@ -148,26 +151,30 @@ class Base extends \Espo\Core\Utils\Database\Orm\Base
|
||||
/**
|
||||
*
|
||||
* @param string $linkName
|
||||
* @param string $entityName
|
||||
* @param string $entityType
|
||||
* @param ?string $foreignLinkName
|
||||
* @param ?string $foreignEntityName
|
||||
* @return array<string,mixed>
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function process($linkName, $entityName, $foreignLinkName, $foreignEntityName)
|
||||
public function process($linkName, $entityType, $foreignLinkName, $foreignEntityName)
|
||||
{
|
||||
$inputs = [
|
||||
'itemName' => $linkName,
|
||||
'entityName' => $entityName,
|
||||
'entityName' => $entityType,
|
||||
'foreignLinkName' => $foreignLinkName,
|
||||
'foreignEntityName' => $foreignEntityName,
|
||||
];
|
||||
|
||||
$this->setMethods($inputs);
|
||||
|
||||
$convertedDefs = $this->load($linkName, $entityName);
|
||||
$convertedDefs = $this->load($linkName, $entityType);
|
||||
|
||||
$convertedDefs = $this->mergeAllowedParams($convertedDefs);
|
||||
|
||||
if (isset($convertedDefs[$entityType]['relations'][$linkName])) {
|
||||
$this->correct($convertedDefs[$entityType]['relations'][$linkName]);
|
||||
}
|
||||
|
||||
$inputs = $this->setArrayValue(null, $inputs);
|
||||
|
||||
$this->setMethods($inputs);
|
||||
@@ -176,8 +183,34 @@ class Base extends \Espo\Core\Utils\Database\Orm\Base
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $loads
|
||||
* @return array<string,mixed>'
|
||||
* @param array<string, mixed> $relationDefs
|
||||
*/
|
||||
private function correct(array &$relationDefs): void
|
||||
{
|
||||
if (!isset($relationDefs['additionalColumns'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var array<string, array<string, mixed>> $additionalColumns */
|
||||
$additionalColumns = &$relationDefs['additionalColumns'];
|
||||
|
||||
foreach ($additionalColumns as &$columnDefs) {
|
||||
$columnDefs['type'] ??= Entity::VARCHAR;
|
||||
|
||||
if (
|
||||
$columnDefs['type'] === Entity::VARCHAR &&
|
||||
!isset($columnDefs['len'])
|
||||
) {
|
||||
$columnDefs['len'] = self::DEFAULT_VARCHAR_LENGTH;
|
||||
}
|
||||
}
|
||||
|
||||
$relationDefs['additionalColumns'] = $additionalColumns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $loads
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function mergeAllowedParams($loads)
|
||||
{
|
||||
@@ -193,7 +226,7 @@ class Base extends \Espo\Core\Utils\Database\Orm\Base
|
||||
if (isset($additionalParam)) {
|
||||
$linkParams[$name] = $additionalParam;
|
||||
|
||||
if (isset($linkParams[$name]) && is_array($linkParams[$name])) {
|
||||
if (is_array($linkParams[$name])) {
|
||||
$linkParams[$name] = Util::merge($linkParams[$name], $additionalParam);
|
||||
}
|
||||
}
|
||||
@@ -205,17 +238,15 @@ class Base extends \Espo\Core\Utils\Database\Orm\Base
|
||||
|
||||
/**
|
||||
* @param string $allowedItemName
|
||||
* @return ?array<string,mixed>
|
||||
* @return ?array<string, mixed>
|
||||
*/
|
||||
private function getAllowedAdditionalParam($allowedItemName)
|
||||
{
|
||||
$linkParams = $this->getLinkParams();
|
||||
$foreignLinkParams = $this->getForeignLinkParams();
|
||||
|
||||
$itemLinkParams = isset($linkParams[$allowedItemName]) ? $linkParams[$allowedItemName] : null;
|
||||
$itemForeignLinkParams = isset($foreignLinkParams[$allowedItemName]) ?
|
||||
$foreignLinkParams[$allowedItemName] :
|
||||
null;
|
||||
$itemLinkParams = $linkParams[$allowedItemName] ?? null;
|
||||
$itemForeignLinkParams = $foreignLinkParams[$allowedItemName] ?? null;
|
||||
|
||||
$additionalParam = null;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class ManyMany extends Base
|
||||
/**
|
||||
* @param string $linkName
|
||||
* @param string $entityType
|
||||
* @return array<string,mixed>
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function load($linkName, $entityType)
|
||||
{
|
||||
@@ -132,7 +132,7 @@ class ManyMany extends Base
|
||||
/**
|
||||
* @param string $entity1
|
||||
* @param string $entity2
|
||||
* @return array{string,string}
|
||||
* @return array{string, string}
|
||||
*/
|
||||
protected function getSortEntities($entity1, $entity2)
|
||||
{
|
||||
@@ -146,4 +146,4 @@ class ManyMany extends Base
|
||||
return $entities;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Schema;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema as DbalSchema;
|
||||
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
abstract class BaseRebuildActions
|
||||
{
|
||||
/** @var ?DbalSchema */
|
||||
protected $currentSchema = null;
|
||||
/** @var ?DbalSchema */
|
||||
protected $metadataSchema = null;
|
||||
|
||||
protected Metadata $metadata;
|
||||
protected Config $config;
|
||||
protected EntityManager $entityManager;
|
||||
protected Log $log;
|
||||
|
||||
public function __construct(
|
||||
Metadata $metadata,
|
||||
Config $config,
|
||||
EntityManager $entityManager,
|
||||
Log $log
|
||||
) {
|
||||
$this->metadata = $metadata;
|
||||
$this->config = $config;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->log = $log;
|
||||
}
|
||||
|
||||
protected function getEntityManager(): EntityManager
|
||||
{
|
||||
return $this->entityManager;
|
||||
}
|
||||
|
||||
protected function getConfig(): Config
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
protected function getMetadata(): Metadata
|
||||
{
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
public function setCurrentSchema(DbalSchema $currentSchema): void
|
||||
{
|
||||
$this->currentSchema = $currentSchema;
|
||||
}
|
||||
|
||||
public function setMetadataSchema(DbalSchema $metadataSchema): void
|
||||
{
|
||||
$this->metadataSchema = $metadataSchema;
|
||||
}
|
||||
|
||||
protected function getCurrentSchema(): ?DbalSchema
|
||||
{
|
||||
return $this->currentSchema;
|
||||
}
|
||||
|
||||
protected function getMetadataSchema(): ?DbalSchema
|
||||
{
|
||||
return $this->metadataSchema;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Schema;
|
||||
|
||||
use Espo\Core\Utils\Database\ConfigDataProvider;
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\Module\PathProvider;
|
||||
use Espo\Core\Utils\Util;
|
||||
|
||||
use Espo\ORM\Defs\AttributeDefs;
|
||||
use Espo\ORM\Defs\EntityDefs;
|
||||
use Espo\ORM\Defs\IndexDefs;
|
||||
use Espo\ORM\Defs\RelationDefs;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\DBAL\Schema\Table;
|
||||
use Doctrine\DBAL\Schema\Schema as DbalSchema;
|
||||
use Doctrine\DBAL\Types\Type as DbalType;
|
||||
|
||||
/**
|
||||
* Schema representation builder.
|
||||
*/
|
||||
class Builder
|
||||
{
|
||||
private const ID_LENGTH = 24; // @todo Make configurable.
|
||||
|
||||
private const ATTR_ID = 'id';
|
||||
private const ATTR_DELETED = 'deleted';
|
||||
|
||||
private string $tablesPath = 'Core/Utils/Database/Schema/tables';
|
||||
/** @var string[] */
|
||||
private $typeList;
|
||||
private ColumnPreparator $columnPreparator;
|
||||
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private FileManager $fileManager,
|
||||
private Log $log,
|
||||
private PathProvider $pathProvider,
|
||||
ConfigDataProvider $configDataProvider,
|
||||
ColumnPreparatorFactory $columnPreparatorFactory
|
||||
) {
|
||||
$this->typeList = array_keys(DbalType::getTypesMap());
|
||||
|
||||
$platform = $configDataProvider->getPlatform();
|
||||
|
||||
$this->columnPreparator = $columnPreparatorFactory->create($platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a schema representation for an ORM metadata.
|
||||
*
|
||||
* @param array<string, mixed> $ormMeta Raw ORM metadata.
|
||||
* @param ?string[] $entityTypeList Specific entity types.
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function build(array $ormMeta, ?array $entityTypeList = null): DbalSchema
|
||||
{
|
||||
$this->log->debug('Schema\Builder - Start');
|
||||
|
||||
$ormMeta = $this->amendMetadata($ormMeta, $entityTypeList);
|
||||
$tables = [];
|
||||
|
||||
$schema = new DbalSchema();
|
||||
|
||||
foreach ($ormMeta as $entityType => $entityParams) {
|
||||
$entityDefs = EntityDefs::fromRaw($entityParams, $entityType);
|
||||
|
||||
$this->buildEntity($entityDefs, $schema, $tables);
|
||||
}
|
||||
|
||||
foreach ($ormMeta as $entityType => $entityParams) {
|
||||
foreach (($entityParams['relations'] ?? []) as $relationName => $relationParams) {
|
||||
$relationDefs = RelationDefs::fromRaw($relationParams, $relationName);
|
||||
|
||||
if ($relationDefs->getType() !== Entity::MANY_MANY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->buildManyMany($entityType, $relationDefs, $schema, $tables);
|
||||
}
|
||||
}
|
||||
|
||||
$this->log->debug('Schema\Builder - End');
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, Table> $tables
|
||||
* @throws SchemaException
|
||||
*/
|
||||
private function buildEntity(EntityDefs $entityDefs, DbalSchema $schema, array &$tables): void
|
||||
{
|
||||
if ($entityDefs->getParam('skipRebuild')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entityType = $entityDefs->getName();
|
||||
|
||||
$this->log->debug("Schema\Builder: Entity {$entityType}");
|
||||
|
||||
$tableName = Util::toUnderScore($entityType);
|
||||
|
||||
if ($schema->hasTable($tableName)) {
|
||||
$tables[$entityType] ??= $schema->getTable($tableName);
|
||||
|
||||
$this->log->debug('Schema\Builder: Table [' . $tableName . '] exists.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$table = $schema->createTable($tableName);
|
||||
|
||||
$tables[$entityType] = $table;
|
||||
|
||||
/** @var array<string, mixed> $tableParams */
|
||||
$tableParams = $entityDefs->getParam('params') ?? [];
|
||||
|
||||
foreach ($tableParams as $paramName => $paramValue) {
|
||||
$table->addOption($paramName, $paramValue);
|
||||
}
|
||||
|
||||
$primaryColumns = [];
|
||||
|
||||
foreach ($entityDefs->getAttributeList() as $attributeDefs) {
|
||||
if (
|
||||
$attributeDefs->isNotStorable() ||
|
||||
$attributeDefs->getType() === Entity::FOREIGN
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$column = $this->columnPreparator->prepare($attributeDefs);
|
||||
|
||||
if ($attributeDefs->getType() === Entity::ID) {
|
||||
$primaryColumns[] = $column->getName();
|
||||
}
|
||||
|
||||
if (!in_array($column->getType(), $this->typeList)) {
|
||||
$this->log->debug(
|
||||
'Schema\Builder: Column type [' . $column->getType() . '] not supported, ' .
|
||||
$entityType . ':' . $attributeDefs->getName()
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($table->hasColumn($column->getName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->addColumn($table, $column);
|
||||
}
|
||||
|
||||
$table->setPrimaryKey($primaryColumns);
|
||||
|
||||
$this->addIndexes($table, $entityDefs->getIndexList());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $ormMeta
|
||||
* @param ?string[] $entityTypeList
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function amendMetadata(array $ormMeta, ?array $entityTypeList): array
|
||||
{
|
||||
/** @var array<string, mixed> $ormMeta */
|
||||
$ormMeta = Util::merge(
|
||||
$ormMeta,
|
||||
$this->getCustomTables()
|
||||
);
|
||||
|
||||
if (isset($ormMeta['unsetIgnore'])) {
|
||||
$protectedOrmMeta = [];
|
||||
|
||||
foreach ($ormMeta['unsetIgnore'] as $protectedKey) {
|
||||
$protectedOrmMeta = Util::merge(
|
||||
$protectedOrmMeta,
|
||||
Util::fillArrayKeys($protectedKey, Util::getValueByKey($ormMeta, $protectedKey))
|
||||
);
|
||||
}
|
||||
|
||||
unset($ormMeta['unsetIgnore']);
|
||||
}
|
||||
|
||||
// Unset some keys.
|
||||
if (isset($ormMeta['unset'])) {
|
||||
/** @var array<string, mixed> $ormMeta */
|
||||
$ormMeta = Util::unsetInArray($ormMeta, $ormMeta['unset']);
|
||||
|
||||
unset($ormMeta['unset']);
|
||||
}
|
||||
|
||||
if (isset($protectedOrmMeta)) {
|
||||
/** @var array<string, mixed> $ormMeta */
|
||||
$ormMeta = Util::merge($ormMeta, $protectedOrmMeta);
|
||||
}
|
||||
|
||||
if (isset($entityTypeList)) {
|
||||
$dependentEntityTypeList = $this->getDependentEntityTypeList($entityTypeList, $ormMeta);
|
||||
|
||||
$this->log->debug(
|
||||
'Schema\Builder: Rebuild for entity types: [' .
|
||||
implode(', ', $entityTypeList) . '] with dependent entity types: [' .
|
||||
implode(', ', $dependentEntityTypeList) . ']'
|
||||
);
|
||||
|
||||
$ormMeta = array_intersect_key($ormMeta, array_flip($dependentEntityTypeList));
|
||||
}
|
||||
|
||||
return $ormMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
private function addColumn(Table $table, Column $column): void
|
||||
{
|
||||
$table->addColumn(
|
||||
$column->getName(),
|
||||
$column->getType(),
|
||||
self::convertColumn($column)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a relation table for the manyMany relation.
|
||||
*
|
||||
* @param string $entityType
|
||||
* @param array<string, Table> $tables
|
||||
* @throws SchemaException
|
||||
*/
|
||||
private function buildManyMany(
|
||||
string $entityType,
|
||||
RelationDefs $relationDefs,
|
||||
DbalSchema $schema,
|
||||
array &$tables
|
||||
): void {
|
||||
|
||||
$relationshipName = $relationDefs->getRelationshipName();
|
||||
|
||||
if (isset($tables[$relationshipName])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tableName = Util::toUnderScore($relationshipName);
|
||||
|
||||
$this->log->debug("Schema\Builder: ManyMany for {$entityType}.{$relationDefs->getName()}");
|
||||
|
||||
if ($schema->hasTable($tableName)) {
|
||||
$this->log->debug('Schema\Builder: Table [' . $tableName . '] exists.');
|
||||
|
||||
$tables[$relationshipName] ??= $schema->getTable($tableName);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$table = $schema->createTable($tableName);
|
||||
|
||||
$idColumn = $this->columnPreparator->prepare(
|
||||
AttributeDefs::fromRaw([
|
||||
'dbType' => Types::BIGINT,
|
||||
'type' => Entity::ID,
|
||||
'len' => 20,
|
||||
'autoincrement' => true,
|
||||
], self::ATTR_ID)
|
||||
);
|
||||
|
||||
$this->addColumn($table, $idColumn);
|
||||
|
||||
if (!$relationDefs->hasMidKey() || !$relationDefs->getForeignMidKey()) {
|
||||
$this->log->error('Schema\Builder: Relationship midKeys are empty.', [
|
||||
'entityType' => $entityType,
|
||||
'relationName' => $relationDefs->getName(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$midKeys = [
|
||||
$relationDefs->getMidKey(),
|
||||
$relationDefs->getForeignMidKey(),
|
||||
];
|
||||
|
||||
foreach ($midKeys as $midKey) {
|
||||
$column = $this->columnPreparator->prepare(
|
||||
AttributeDefs::fromRaw([
|
||||
'dbType' => Entity::VARCHAR,
|
||||
'type' => Entity::FOREIGN_ID,
|
||||
'len' => self::ID_LENGTH,
|
||||
], $midKey)
|
||||
);
|
||||
|
||||
$this->addColumn($table, $column);
|
||||
}
|
||||
|
||||
/** @var array<string, array<string, mixed>> $additionalColumns */
|
||||
$additionalColumns = $relationDefs->getParam('additionalColumns') ?? [];
|
||||
|
||||
foreach ($additionalColumns as $fieldName => $fieldParams) {
|
||||
$column = $this->columnPreparator->prepare(
|
||||
AttributeDefs::fromRaw($fieldParams, $fieldName)
|
||||
);
|
||||
|
||||
$this->addColumn($table, $column);
|
||||
}
|
||||
|
||||
$deletedColumn = $this->columnPreparator->prepare(
|
||||
AttributeDefs::fromRaw([
|
||||
'type' => Entity::BOOL,
|
||||
'default' => false,
|
||||
], self::ATTR_DELETED)
|
||||
);
|
||||
|
||||
$this->addColumn($table, $deletedColumn);
|
||||
|
||||
$table->setPrimaryKey([self::ATTR_ID]);
|
||||
|
||||
$this->addIndexes($table, $relationDefs->getIndexList());
|
||||
|
||||
$tables[$relationshipName] = $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IndexDefs[] $indexDefsList
|
||||
* @throws SchemaException
|
||||
*/
|
||||
private function addIndexes(Table $table, array $indexDefsList): void
|
||||
{
|
||||
foreach ($indexDefsList as $indexDefs) {
|
||||
$columns = array_map(
|
||||
fn($item) => Util::toUnderScore($item),
|
||||
$indexDefs->getColumnList()
|
||||
);
|
||||
|
||||
if ($indexDefs->isUnique()) {
|
||||
$table->addUniqueIndex($columns, $indexDefs->getKey());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$table->addIndex($columns, $indexDefs->getKey(), $indexDefs->getFlagList());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Move to a class. Add unit test.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function convertColumn(Column $column): array
|
||||
{
|
||||
$result = [
|
||||
'notnull' => $column->isNotNull(),
|
||||
];
|
||||
|
||||
if ($column->getLength() !== null) {
|
||||
$result['length'] = $column->getLength();
|
||||
}
|
||||
|
||||
if ($column->getDefault() !== null) {
|
||||
$result['default'] = $column->getDefault();
|
||||
}
|
||||
|
||||
if ($column->getAutoincrement() !== null) {
|
||||
$result['autoincrement'] = $column->getAutoincrement();
|
||||
}
|
||||
|
||||
if ($column->getPrecision() !== null) {
|
||||
$result['precision'] = $column->getPrecision();
|
||||
}
|
||||
|
||||
if ($column->getScale() !== null) {
|
||||
$result['scale'] = $column->getScale();
|
||||
}
|
||||
|
||||
if ($column->getUnsigned() !== null) {
|
||||
$result['unsigned'] = $column->getUnsigned();
|
||||
}
|
||||
|
||||
// Can't use customSchemaOptions as it causes unwanted ALTER TABLE.
|
||||
$result['platformOptions'] = [];
|
||||
|
||||
if ($column->getCollation()) {
|
||||
$result['platformOptions']['collation'] = $column->getCollation();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom table definition in `application/Espo/Core/Utils/Database/Schema/tables`.
|
||||
* This logic can be removed in the future. Usage of table files in not recommended.
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function getCustomTables(): array
|
||||
{
|
||||
$customTables = $this->loadData($this->pathProvider->getCore() . $this->tablesPath);
|
||||
|
||||
foreach ($this->metadata->getModuleList() as $moduleName) {
|
||||
$modulePath = $this->pathProvider->getModule($moduleName) . $this->tablesPath;
|
||||
|
||||
$customTables = Util::merge(
|
||||
$customTables,
|
||||
$this->loadData($modulePath)
|
||||
);
|
||||
}
|
||||
|
||||
/** @var array<string, mixed> $customTables */
|
||||
$customTables = Util::merge(
|
||||
$customTables,
|
||||
$this->loadData($this->pathProvider->getCustom() . $this->tablesPath)
|
||||
);
|
||||
|
||||
return $customTables;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $entityTypeList
|
||||
* @param array<string, mixed> $ormMeta
|
||||
* @param string[] $depList
|
||||
* @return string[]
|
||||
*/
|
||||
private function getDependentEntityTypeList(array $entityTypeList, array $ormMeta, array $depList = []): array
|
||||
{
|
||||
foreach ($entityTypeList as $entityType) {
|
||||
if (in_array($entityType, $depList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$depList[] = $entityType;
|
||||
|
||||
$entityDefs = EntityDefs::fromRaw($ormMeta[$entityType] ?? [], $entityType);
|
||||
|
||||
foreach ($entityDefs->getRelationList() as $relationDefs) {
|
||||
if (!$relationDefs->hasForeignEntityType()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$itemEntityType = $relationDefs->getForeignEntityType();
|
||||
|
||||
if (in_array($itemEntityType, $depList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$depList = $this->getDependentEntityTypeList([$itemEntityType], $ormMeta, $depList);
|
||||
}
|
||||
}
|
||||
|
||||
return $depList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function loadData(string $path): array
|
||||
{
|
||||
$tables = [];
|
||||
|
||||
if (!file_exists($path)) {
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/** @var string[] $fileList */
|
||||
$fileList = $this->fileManager->getFileList($path, false, '\.php$', true);
|
||||
|
||||
foreach ($fileList as $fileName) {
|
||||
$itemPath = $path . '/' . $fileName;
|
||||
|
||||
if (!$this->fileManager->isFile($itemPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fileData = $this->fileManager->getPhpContents($itemPath);
|
||||
|
||||
if (!is_array($fileData)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var array<string, array<string, mixed>> $tables */
|
||||
$tables = Util::merge($tables, $fileData);
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Schema;
|
||||
|
||||
class Column
|
||||
{
|
||||
private bool $notNull = false;
|
||||
private ?int $length = null;
|
||||
private mixed $default = null;
|
||||
private ?bool $autoincrement = null;
|
||||
private ?int $precision = null;
|
||||
private ?int $scale = null;
|
||||
private ?bool $unsigned = null;
|
||||
private ?string $collation = null;
|
||||
|
||||
private function __construct(
|
||||
private string $name,
|
||||
private string $type
|
||||
) {}
|
||||
|
||||
public static function create(string $name, string $type): self
|
||||
{
|
||||
return new self($name, $type);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function isNotNull(): bool
|
||||
{
|
||||
return $this->notNull;
|
||||
}
|
||||
|
||||
public function getLength(): ?int
|
||||
{
|
||||
return $this->length;
|
||||
}
|
||||
|
||||
public function getDefault(): mixed
|
||||
{
|
||||
return $this->default;
|
||||
}
|
||||
|
||||
public function getAutoincrement(): ?bool
|
||||
{
|
||||
return $this->autoincrement;
|
||||
}
|
||||
|
||||
public function getUnsigned(): ?bool
|
||||
{
|
||||
return $this->unsigned;
|
||||
}
|
||||
|
||||
public function getPrecision(): ?int
|
||||
{
|
||||
return $this->precision;
|
||||
}
|
||||
|
||||
public function getScale(): ?int
|
||||
{
|
||||
return $this->scale;
|
||||
}
|
||||
|
||||
public function getCollation(): ?string
|
||||
{
|
||||
return $this->collation;
|
||||
}
|
||||
|
||||
public function withNotNull(bool $notNull = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->notNull = $notNull;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withLength(?int $length): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->length = $length;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withDefault(mixed $default): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->default = $default;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withAutoincrement(?bool $autoincrement = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->autoincrement = $autoincrement;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported only by MySQL.
|
||||
*/
|
||||
public function withUnsigned(?bool $unsigned = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->unsigned = $unsigned;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withPrecision(?int $precision): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->precision = $precision;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withScale(?int $scale): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->scale = $scale;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withCollation(?string $collation): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->collation = $collation;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Schema;
|
||||
|
||||
use Espo\ORM\Defs\AttributeDefs;
|
||||
|
||||
interface ColumnPreparator
|
||||
{
|
||||
public function prepare(AttributeDefs $defs): Column;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Schema;
|
||||
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Database\Helper;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use RuntimeException;
|
||||
|
||||
class ColumnPreparatorFactory
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private Helper $helper
|
||||
) {}
|
||||
|
||||
public function create(string $platform): ColumnPreparator
|
||||
{
|
||||
/** @var ?class-string<ColumnPreparator> $className */
|
||||
$className = $this->metadata
|
||||
->get(['app', 'database', 'platforms', $platform, 'columnPreparatorClassName']);
|
||||
|
||||
if (!$className) {
|
||||
throw new RuntimeException("No Column-Preparator for {$platform}.");
|
||||
}
|
||||
|
||||
$binding = BindingContainerBuilder::create()
|
||||
->bindInstance(Helper::class, $this->helper)
|
||||
->build();
|
||||
|
||||
return $this->injectableFactory->createWithBinding($className, $binding);
|
||||
}
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Schema\ColumnPreparators;
|
||||
|
||||
use Espo\Core\Utils\Database\Helper;
|
||||
use Espo\Core\Utils\Database\Schema\Column;
|
||||
use Espo\Core\Utils\Database\Schema\ColumnPreparator;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\ORM\Defs\AttributeDefs;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class MysqlColumnPreparator implements ColumnPreparator
|
||||
{
|
||||
private const PARAM_DB_TYPE = 'dbType';
|
||||
private const PARAM_DEFAULT = 'default';
|
||||
private const PARAM_NOT_NULL = 'notNull';
|
||||
private const PARAM_AUTOINCREMENT = 'autoincrement';
|
||||
private const PARAM_PRECISION = 'precision';
|
||||
private const PARAM_SCALE = 'scale';
|
||||
private const PARAM_BINARY = 'binary';
|
||||
|
||||
public const TYPE_MYSQL = 'MySQL';
|
||||
public const TYPE_MARIADB = 'MariaDB';
|
||||
|
||||
private const MB4_INDEX_LENGTH_LIMIT = 3072;
|
||||
private const DEFAULT_INDEX_LIMIT = 1000;
|
||||
|
||||
private ?int $maxIndexLength = null;
|
||||
|
||||
public function __construct(
|
||||
private Helper $helper
|
||||
) {}
|
||||
|
||||
public function prepare(AttributeDefs $defs): Column
|
||||
{
|
||||
$columnType = $defs->getParam(self::PARAM_DB_TYPE) ?? $defs->getType();
|
||||
$columnName = Util::toUnderScore($defs->getName());
|
||||
|
||||
$column = Column::create($columnName, strtolower($columnType));
|
||||
|
||||
$type = $defs->getType();
|
||||
$length = $defs->getLength();
|
||||
$default = $defs->getParam(self::PARAM_DEFAULT);
|
||||
$notNull = $defs->getParam(self::PARAM_NOT_NULL);
|
||||
$autoincrement = $defs->getParam(self::PARAM_AUTOINCREMENT);
|
||||
$precision = $defs->getParam(self::PARAM_PRECISION);
|
||||
$scale = $defs->getParam(self::PARAM_SCALE);
|
||||
$binary = $defs->getParam(self::PARAM_BINARY);
|
||||
|
||||
if ($length !== null) {
|
||||
$column = $column->withLength($length);
|
||||
}
|
||||
|
||||
if ($default !== null) {
|
||||
$column = $column->withDefault($default);
|
||||
}
|
||||
|
||||
if ($notNull !== null) {
|
||||
$column = $column->withNotNull($notNull);
|
||||
}
|
||||
|
||||
if ($autoincrement !== null) {
|
||||
$column = $column->withAutoincrement($autoincrement);
|
||||
}
|
||||
|
||||
if ($precision !== null) {
|
||||
$column = $column->withPrecision($precision);
|
||||
}
|
||||
|
||||
if ($scale !== null) {
|
||||
$column = $column->withScale($scale);
|
||||
}
|
||||
|
||||
$mb3 = false;
|
||||
|
||||
switch ($type) {
|
||||
case Entity::ID:
|
||||
case Entity::FOREIGN_ID:
|
||||
case Entity::FOREIGN_TYPE:
|
||||
$mb3 = $this->getMaxIndexLength() < self::MB4_INDEX_LENGTH_LIMIT;
|
||||
|
||||
break;
|
||||
|
||||
case Entity::TEXT:
|
||||
case Entity::JSON_ARRAY:
|
||||
$column = $column->withDefault(null);
|
||||
|
||||
break;
|
||||
|
||||
case Entity::BOOL:
|
||||
$default = intval($default ?? false);
|
||||
|
||||
$column = $column->withDefault($default);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($type !== Entity::ID && $autoincrement) {
|
||||
$column = $column
|
||||
->withNotNull()
|
||||
->withUnsigned();
|
||||
}
|
||||
|
||||
$collation = $binary ?
|
||||
'utf8mb4_bin' :
|
||||
'utf8mb4_unicode_ci';
|
||||
|
||||
if ($mb3) {
|
||||
$collation = $binary ?
|
||||
'utf8_bin' :
|
||||
'utf8_unicode_ci';
|
||||
}
|
||||
|
||||
return $column->withCollation($collation);
|
||||
}
|
||||
|
||||
private function getMaxIndexLength(): int
|
||||
{
|
||||
if (!isset($this->maxIndexLength)) {
|
||||
$this->maxIndexLength = $this->detectMaxIndexLength();
|
||||
}
|
||||
|
||||
return $this->maxIndexLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximum index length.
|
||||
*/
|
||||
private function detectMaxIndexLength(): int
|
||||
{
|
||||
$databaseType = $this->helper->getType();
|
||||
|
||||
$tableEngine = $this->getTableEngine();
|
||||
|
||||
if (!$tableEngine) {
|
||||
return self::DEFAULT_INDEX_LIMIT;
|
||||
}
|
||||
|
||||
switch ($tableEngine) {
|
||||
case 'InnoDB':
|
||||
$version = $this->helper->getVersion();
|
||||
|
||||
switch ($databaseType) {
|
||||
case self::TYPE_MARIADB:
|
||||
if (version_compare($version, '10.2.2') >= 0) {
|
||||
return 3072; // InnoDB, MariaDB 10.2.2+
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case self::TYPE_MYSQL:
|
||||
return 3072;
|
||||
}
|
||||
|
||||
return 767; // InnoDB
|
||||
}
|
||||
|
||||
return 1000; // MyISAM
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a table or default engine.
|
||||
*/
|
||||
private function getTableEngine(): ?string
|
||||
{
|
||||
$databaseType = $this->helper->getType();
|
||||
|
||||
if (!in_array($databaseType, [self::TYPE_MYSQL, self::TYPE_MARIADB])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$query = "SHOW TABLE STATUS WHERE Engine = 'MyISAM'";
|
||||
|
||||
$vars = [];
|
||||
|
||||
$pdo = $this->helper->getPDO();
|
||||
|
||||
$sth = $pdo->prepare($query);
|
||||
$sth->execute($vars);
|
||||
|
||||
$result = $sth->fetchColumn();
|
||||
|
||||
if (!empty($result)) {
|
||||
return 'MyISAM';
|
||||
}
|
||||
|
||||
return 'InnoDB';
|
||||
}
|
||||
}
|
||||
@@ -1,645 +0,0 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Schema;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Database\Schema\Utils as SchemaUtils;
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\Module\PathProvider;
|
||||
use Espo\Core\Utils\Util;
|
||||
|
||||
use Doctrine\DBAL\Schema\Table;
|
||||
use Doctrine\DBAL\Schema\Schema as DbalSchema;
|
||||
use Doctrine\DBAL\Types\Type as DbalType;
|
||||
use Espo\ORM\Defs\IndexDefs;
|
||||
|
||||
class Converter
|
||||
{
|
||||
private ?DbalSchema $dbalSchema = null;
|
||||
private Schema $databaseSchema;
|
||||
private FileManager $fileManager;
|
||||
private Metadata $metadata;
|
||||
private Config $config;
|
||||
private Log $log;
|
||||
private PathProvider $pathProvider;
|
||||
|
||||
private string $tablesPath = 'Core/Utils/Database/Schema/tables';
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $typeList;
|
||||
|
||||
/**
|
||||
* ORM => doctrine
|
||||
* @var array<string,string>
|
||||
*/
|
||||
private $allowedDbFieldParams = [
|
||||
'len' => 'length',
|
||||
'default' => 'default',
|
||||
'notNull' => 'notnull',
|
||||
'autoincrement' => 'autoincrement',
|
||||
'precision' => 'precision',
|
||||
'scale' => 'scale',
|
||||
];
|
||||
|
||||
/**
|
||||
* @todo Same array in Converters\Orm.
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
private $idParams = [
|
||||
'dbType' => 'varchar',
|
||||
'len' => 24,
|
||||
];
|
||||
|
||||
/**
|
||||
* @todo Same array in Converters\Orm.
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
private $defaultLength = [
|
||||
'varchar' => 255,
|
||||
'int' => 11,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $notStorableTypes = [
|
||||
'foreign',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var ?int
|
||||
*/
|
||||
private $maxIndexLength = null;
|
||||
|
||||
public function __construct(
|
||||
Metadata $metadata,
|
||||
FileManager $fileManager,
|
||||
Schema $databaseSchema,
|
||||
Config $config,
|
||||
Log $log,
|
||||
PathProvider $pathProvider
|
||||
) {
|
||||
$this->metadata = $metadata;
|
||||
$this->fileManager = $fileManager;
|
||||
$this->databaseSchema = $databaseSchema;
|
||||
$this->config = $config;
|
||||
$this->log = $log;
|
||||
$this->pathProvider = $pathProvider;
|
||||
|
||||
$this->typeList = array_keys(DbalType::getTypesMap());
|
||||
}
|
||||
|
||||
private function getSchema(bool $reload = false): DbalSchema
|
||||
{
|
||||
if (!isset($this->dbalSchema) || $reload) {
|
||||
$this->dbalSchema = new DbalSchema();
|
||||
}
|
||||
|
||||
return $this->dbalSchema;
|
||||
}
|
||||
|
||||
private function getDatabaseSchema(): Schema
|
||||
{
|
||||
return $this->databaseSchema;
|
||||
}
|
||||
|
||||
private function getMaxIndexLength(): int
|
||||
{
|
||||
if (!isset($this->maxIndexLength)) {
|
||||
$this->maxIndexLength = $this->getDatabaseSchema()
|
||||
->getDatabaseHelper()
|
||||
->getMaxIndexLength();
|
||||
}
|
||||
|
||||
return $this->maxIndexLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema conversation process.
|
||||
*
|
||||
* @param array<string,mixed> $ormMeta
|
||||
* @param string[]|string|null $entityList
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function process(array $ormMeta, $entityList = null): DbalSchema
|
||||
{
|
||||
$this->log->debug('Schema\Converter - Start: building schema');
|
||||
|
||||
// Check if exist files in "Tables" directory and merge with ormMetadata.
|
||||
|
||||
/** @var array<string, mixed> $ormMeta */
|
||||
$ormMeta = Util::merge($ormMeta, $this->getCustomTables($ormMeta));
|
||||
|
||||
if (isset($ormMeta['unsetIgnore'])) {
|
||||
$protectedOrmMeta = [];
|
||||
|
||||
foreach ($ormMeta['unsetIgnore'] as $protectedKey) {
|
||||
$protectedOrmMeta = Util::merge(
|
||||
$protectedOrmMeta,
|
||||
Util::fillArrayKeys($protectedKey, Util::getValueByKey($ormMeta, $protectedKey))
|
||||
);
|
||||
}
|
||||
|
||||
unset($ormMeta['unsetIgnore']);
|
||||
}
|
||||
|
||||
// unset some keys in orm
|
||||
if (isset($ormMeta['unset'])) {
|
||||
/** @var array<string, mixed> $ormMeta */
|
||||
$ormMeta = Util::unsetInArray($ormMeta, $ormMeta['unset']);
|
||||
|
||||
unset($ormMeta['unset']);
|
||||
}
|
||||
|
||||
if (isset($protectedOrmMeta)) {
|
||||
/** @var array<string, mixed> $ormMeta */
|
||||
$ormMeta = Util::merge($ormMeta, $protectedOrmMeta);
|
||||
}
|
||||
|
||||
if (isset($entityList)) {
|
||||
$entityList = is_string($entityList) ? (array) $entityList : $entityList;
|
||||
|
||||
$dependentEntities = $this->getDependentEntities($entityList, $ormMeta);
|
||||
|
||||
$this->log->debug(
|
||||
'Rebuild Database for entities: [' .
|
||||
implode(', ', $entityList) . '] with dependent entities: [' .
|
||||
implode(', ', $dependentEntities) . ']'
|
||||
);
|
||||
|
||||
$ormMeta = array_intersect_key($ormMeta, array_flip($dependentEntities));
|
||||
}
|
||||
|
||||
$schema = $this->getSchema(true);
|
||||
|
||||
$indexes = SchemaUtils::getIndexes($ormMeta);
|
||||
|
||||
$tables = [];
|
||||
|
||||
foreach ($ormMeta as $entityName => $entityParams) {
|
||||
if ($entityParams['skipRebuild'] ?? false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tableName = Util::toUnderScore($entityName);
|
||||
|
||||
if ($schema->hasTable($tableName)) {
|
||||
if (!isset($tables[$entityName])) {
|
||||
$tables[$entityName] = $schema->getTable($tableName);
|
||||
}
|
||||
|
||||
$this->log->debug('DBAL: Table ['.$tableName.'] exists.');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$tables[$entityName] = $schema->createTable($tableName);
|
||||
|
||||
if (isset($entityParams['params']) && is_array($entityParams['params'])) {
|
||||
foreach ($entityParams['params'] as $paramName => $paramValue) {
|
||||
$tables[$entityName]->addOption($paramName, $paramValue);
|
||||
}
|
||||
}
|
||||
|
||||
$primaryColumns = [];
|
||||
|
||||
foreach ($entityParams['fields'] as $fieldName => $fieldParams) {
|
||||
if (
|
||||
(isset($fieldParams['notStorable']) && $fieldParams['notStorable']) ||
|
||||
in_array($fieldParams['type'], $this->notStorableTypes)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($fieldParams['type']) {
|
||||
case 'id':
|
||||
$primaryColumns[] = Util::toUnderScore($fieldName);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$fieldType = $fieldParams['dbType'] ?? $fieldParams['type'];
|
||||
|
||||
/** Doctrine uses 'strtolower' for all field types. */
|
||||
$fieldType = strtolower($fieldType);
|
||||
|
||||
if (!in_array($fieldType, $this->typeList)) {
|
||||
$this->log->debug(
|
||||
'Converters\Schema::process(): Field type [' . $fieldType . '] does not exist '.
|
||||
$entityName.':'.$fieldName
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$columnName = Util::toUnderScore($fieldName);
|
||||
|
||||
if (!$tables[$entityName]->hasColumn($columnName)) {
|
||||
$tables[$entityName]->addColumn($columnName, $fieldType, $this->getDbFieldParams($fieldParams));
|
||||
}
|
||||
}
|
||||
|
||||
$tables[$entityName]->setPrimaryKey($primaryColumns);
|
||||
|
||||
if (!empty($indexes[$entityName])) {
|
||||
$this->addIndexes($tables[$entityName], $indexes[$entityName]);
|
||||
}
|
||||
}
|
||||
|
||||
// Check and create columns/tables for relations.
|
||||
foreach ($ormMeta as $entityName => $entityParams) {
|
||||
if (!isset($entityParams['relations'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($entityParams['relations'] as $relationName => $relationParams) {
|
||||
switch ($relationParams['type']) {
|
||||
case 'manyMany':
|
||||
$tableName = $relationParams['relationName'];
|
||||
|
||||
// Check for duplicate tables.
|
||||
if (!isset($tables[$tableName])) {
|
||||
// No needs to create a table if it already exists.
|
||||
$tables[$tableName] = $this->prepareManyMany($entityName, $relationParams);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->log->debug('Schema\Converter - End: building schema');
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a relation table for the manyMany relation.
|
||||
*
|
||||
* @param string $entityName
|
||||
* @param array<string, mixed> $relationParams
|
||||
* @throws SchemaException
|
||||
*/
|
||||
private function prepareManyMany(string $entityName, $relationParams): Table
|
||||
{
|
||||
$relationName = $relationParams['relationName'];
|
||||
|
||||
$tableName = Util::toUnderScore($relationName);
|
||||
|
||||
$this->log->debug('DBAL: prepareManyMany invoked for ' . $entityName, [
|
||||
'tableName' => $tableName, 'parameters' => $relationParams
|
||||
]);
|
||||
|
||||
if ($this->getSchema()->hasTable($tableName)) {
|
||||
$this->log->debug('DBAL: Table ['.$tableName.'] exists.');
|
||||
|
||||
return $this->getSchema()->getTable($tableName);
|
||||
}
|
||||
|
||||
$table = $this->getSchema()->createTable($tableName);
|
||||
|
||||
$idColumnOptions = $this->getDbFieldParams([
|
||||
'type' => 'id',
|
||||
'len' => 20,
|
||||
'autoincrement' => true,
|
||||
]);
|
||||
|
||||
$table->addColumn('id', 'bigint', $idColumnOptions);
|
||||
|
||||
// Add midKeys to the schema.
|
||||
$uniqueIndex = [];
|
||||
|
||||
if (empty($relationParams['midKeys'])) {
|
||||
$this->log->debug('REBUILD: midKeys are empty!', [
|
||||
'scope' => $entityName, 'tableName' => $tableName,
|
||||
'parameters' => $relationParams
|
||||
]);
|
||||
}
|
||||
else {
|
||||
foreach ($relationParams['midKeys'] as $midKey) {
|
||||
$columnName = Util::toUnderScore($midKey);
|
||||
|
||||
$table->addColumn(
|
||||
$columnName,
|
||||
$this->idParams['dbType'],
|
||||
$this->getDbFieldParams([
|
||||
'type' => 'foreignId',
|
||||
'len' => $this->idParams['len'],
|
||||
])
|
||||
);
|
||||
|
||||
$indexDefs = IndexDefs::fromRaw([], $columnName);
|
||||
|
||||
$indexName = SchemaUtils::generateIndexName($indexDefs, $relationName);
|
||||
|
||||
$table->addIndex([$columnName], $indexName);
|
||||
|
||||
$uniqueIndex[] = $columnName;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($relationParams['additionalColumns'])) {
|
||||
foreach($relationParams['additionalColumns'] as $fieldName => $fieldParams) {
|
||||
if (!isset($fieldParams['type'])) {
|
||||
$fieldParams = array_merge($fieldParams, [
|
||||
'type' => 'varchar',
|
||||
'len' => $this->defaultLength['varchar'],
|
||||
]);
|
||||
}
|
||||
|
||||
$table->addColumn(
|
||||
Util::toUnderScore($fieldName),
|
||||
$fieldParams['type'],
|
||||
$this->getDbFieldParams($fieldParams)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$table->addColumn(
|
||||
'deleted',
|
||||
'bool',
|
||||
$this->getDbFieldParams([
|
||||
'type' => 'bool',
|
||||
'default' => false,
|
||||
])
|
||||
);
|
||||
|
||||
$table->setPrimaryKey(['id']);
|
||||
|
||||
// Add defined indexes.
|
||||
if (!empty($relationParams['indexes'])) {
|
||||
$normalizedIndexes = SchemaUtils::getIndexes([
|
||||
$entityName => [
|
||||
'indexes' => $relationParams['indexes']
|
||||
]
|
||||
]);
|
||||
|
||||
$this->addIndexes($table, $normalizedIndexes[$entityName]);
|
||||
}
|
||||
|
||||
// Add unique indexes.
|
||||
if (!empty($relationParams['conditions'])) {
|
||||
foreach ($relationParams['conditions'] as $fieldName => $fieldParams) {
|
||||
$uniqueIndex[] = Util::toUnderScore($fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($uniqueIndex)) {
|
||||
/** @var string[] $uniqueIndex */
|
||||
$uniqueIndexName = implode('_', $uniqueIndex);
|
||||
|
||||
$indexDefs = IndexDefs::fromRaw(['type' => 'unique'], $uniqueIndexName);
|
||||
|
||||
$indexName = SchemaUtils::generateIndexName($indexDefs, $relationName);
|
||||
|
||||
$table->addUniqueIndex($uniqueIndex, $indexName);
|
||||
}
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, mixed>> $indexes
|
||||
* @throws SchemaException
|
||||
*/
|
||||
private function addIndexes(Table $table, array $indexes): void
|
||||
{
|
||||
foreach ($indexes as $indexName => $indexParams) {
|
||||
$indexDefs = IndexDefs::fromRaw($indexParams, $indexName);
|
||||
|
||||
if ($indexDefs->isUnique()) {
|
||||
$table->addUniqueIndex($indexDefs->getColumnList(), $indexName);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$table->addIndex($indexDefs->getColumnList(), $indexName, $indexDefs->getFlagList());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $fieldParams
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function getDbFieldParams($fieldParams)
|
||||
{
|
||||
$dbFieldParams = [
|
||||
'notnull' => false,
|
||||
];
|
||||
|
||||
foreach ($this->allowedDbFieldParams as $espoName => $dbalName) {
|
||||
if (isset($fieldParams[$espoName])) {
|
||||
$dbFieldParams[$dbalName] = $fieldParams[$espoName];
|
||||
}
|
||||
}
|
||||
|
||||
$databaseParams = $this->config->get('database');
|
||||
|
||||
if (!isset($databaseParams['charset']) || $databaseParams['charset'] == 'utf8mb4') {
|
||||
$dbFieldParams['platformOptions'] = [
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
];
|
||||
}
|
||||
|
||||
switch ($fieldParams['type']) {
|
||||
case 'id':
|
||||
case 'foreignId':
|
||||
case 'foreignType':
|
||||
if ($this->getMaxIndexLength() < 3072) {
|
||||
$fieldParams['utf8mb3'] = true;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'array':
|
||||
case 'jsonArray':
|
||||
case 'text':
|
||||
case 'longtext':
|
||||
unset($dbFieldParams['default']); // for db type TEXT can't be defined a default value
|
||||
|
||||
break;
|
||||
|
||||
case 'bool':
|
||||
$default = false;
|
||||
|
||||
if (array_key_exists('default', $dbFieldParams)) {
|
||||
$default = $dbFieldParams['default'];
|
||||
}
|
||||
|
||||
$dbFieldParams['default'] = intval($default);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
$fieldParams['type'] != 'id' &&
|
||||
isset($fieldParams['autoincrement']) &&
|
||||
$fieldParams['autoincrement']
|
||||
) {
|
||||
$dbFieldParams['notnull'] = true;
|
||||
$dbFieldParams['unsigned'] = true;
|
||||
}
|
||||
|
||||
if (isset($fieldParams['binary']) && $fieldParams['binary']) {
|
||||
$dbFieldParams['platformOptions'] = [
|
||||
'collation' => 'utf8mb4_bin',
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($fieldParams['utf8mb3']) && $fieldParams['utf8mb3']) {
|
||||
$dbFieldParams['platformOptions'] = [
|
||||
'collation' =>
|
||||
(isset($fieldParams['binary']) && $fieldParams['binary']) ?
|
||||
'utf8_bin' :
|
||||
'utf8_unicode_ci',
|
||||
];
|
||||
}
|
||||
|
||||
return $dbFieldParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom table definition in
|
||||
* `application/Espo/Core/Utils/Database/Schema/tables/` and in metadata 'additionalTables'.
|
||||
*
|
||||
* @param array<string,mixed> $ormMeta
|
||||
* @return array<string,array<string,mixed>>
|
||||
*/
|
||||
private function getCustomTables(array $ormMeta): array
|
||||
{
|
||||
$customTables = $this->loadData($this->pathProvider->getCore() . $this->tablesPath);
|
||||
|
||||
foreach ($this->metadata->getModuleList() as $moduleName) {
|
||||
$modulePath = $this->pathProvider->getModule($moduleName) . $this->tablesPath;
|
||||
|
||||
$customTables = Util::merge(
|
||||
$customTables,
|
||||
$this->loadData($modulePath)
|
||||
);
|
||||
}
|
||||
|
||||
/** @var array<string,mixed> $customTables */
|
||||
$customTables = Util::merge(
|
||||
$customTables,
|
||||
$this->loadData($this->pathProvider->getCustom() . $this->tablesPath)
|
||||
);
|
||||
|
||||
// Get custom tables from metadata 'additionalTables'.
|
||||
foreach ($ormMeta as $entityParams) {
|
||||
if (isset($entityParams['additionalTables']) && is_array($entityParams['additionalTables'])) {
|
||||
/** @var array<string,mixed> $customTables */
|
||||
$customTables = Util::merge($customTables, $entityParams['additionalTables']);
|
||||
}
|
||||
}
|
||||
|
||||
return $customTables;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string[]|string $entityList
|
||||
* @param array<string,mixed> $ormMeta
|
||||
* @param string[] $dependentEntities
|
||||
* @return string[]
|
||||
*/
|
||||
private function getDependentEntities($entityList, $ormMeta, $dependentEntities = [])
|
||||
{
|
||||
if (is_string($entityList)) {
|
||||
$entityList = (array) $entityList;
|
||||
}
|
||||
|
||||
foreach ($entityList as $entityName) {
|
||||
if (in_array($entityName, $dependentEntities)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dependentEntities[] = $entityName;
|
||||
|
||||
if (array_key_exists('relations', $ormMeta[$entityName])) {
|
||||
foreach ($ormMeta[$entityName]['relations'] as $relationName => $relationParams) {
|
||||
if (isset($relationParams['entity'])) {
|
||||
$relationEntity = $relationParams['entity'];
|
||||
|
||||
if (!in_array($relationEntity, $dependentEntities)) {
|
||||
$dependentEntities = $this->getDependentEntities(
|
||||
$relationEntity,
|
||||
$ormMeta,
|
||||
$dependentEntities
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $dependentEntities;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function loadData(string $path): array
|
||||
{
|
||||
$tables = [];
|
||||
|
||||
if (!file_exists($path)) {
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/** @var string[] $fileList */
|
||||
$fileList = $this->fileManager->getFileList($path, false, '\.php$', true);
|
||||
|
||||
foreach ($fileList as $fileName) {
|
||||
$itemPath = $path . '/' . $fileName;
|
||||
|
||||
if (!$this->fileManager->isFile($itemPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fileData = $this->fileManager->getPhpContents($itemPath);
|
||||
|
||||
if (!is_array($fileData)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var array<string,array<string,mixed>> $tables */
|
||||
$tables = Util::merge($tables, $fileData);
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Schema;
|
||||
|
||||
use Espo\Core\Utils\Database\ConfigDataProvider;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
class MetadataProvider
|
||||
{
|
||||
public function __construct(
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
private Metadata $metadata
|
||||
) {}
|
||||
|
||||
private function getPlatform(): string
|
||||
{
|
||||
return $this->configDataProvider->getPlatform();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<RebuildAction>[]
|
||||
*/
|
||||
public function getPreRebuildActionClassNameList(): array
|
||||
{
|
||||
/** @var class-string<RebuildAction>[] */
|
||||
return $this->metadata
|
||||
->get(['app', 'database', 'platforms', $this->getPlatform(), 'preRebuildActionClassNameList']) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<RebuildAction>[]
|
||||
*/
|
||||
public function getPostRebuildActionClassNameList(): array
|
||||
{
|
||||
/** @var class-string<RebuildAction>[] */
|
||||
return $this->metadata
|
||||
->get(['app', 'database', 'platforms', $this->getPlatform(), 'postRebuildActionClassNameList']) ?? [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Schema;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema as DbalSchema;
|
||||
|
||||
interface RebuildAction
|
||||
{
|
||||
public function process(DbalSchema $oldSchema, DbalSchema $newSchema): void;
|
||||
}
|
||||
+24
-40
@@ -27,51 +27,36 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Schema\rebuildActions;
|
||||
namespace Espo\Core\Utils\Database\Schema\RebuildActions;
|
||||
|
||||
use Doctrine\DBAL\Exception as DbalException;
|
||||
use Doctrine\DBAL\Schema\Schema as DbalSchema;
|
||||
use Espo\Core\Utils\Database\Helper;
|
||||
use Espo\Core\Utils\Database\Schema\BaseRebuildActions;
|
||||
use Espo\Core\Utils\Database\Schema\RebuildAction;
|
||||
use Espo\Core\Utils\Log;
|
||||
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
use Espo\Core\Di;
|
||||
|
||||
class FulltextIndex extends BaseRebuildActions implements Di\InjectableFactoryAware
|
||||
class PrepareForFulltextIndex implements RebuildAction
|
||||
{
|
||||
use Di\InjectableFactorySetter;
|
||||
public function __construct(
|
||||
private Helper $helper,
|
||||
private Log $log
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws \Doctrine\DBAL\Exception
|
||||
* @throws DbalException
|
||||
*/
|
||||
public function beforeRebuild()
|
||||
public function process(DbalSchema $oldSchema, DbalSchema $newSchema): void
|
||||
{
|
||||
$currentSchema = $this->getCurrentSchema();
|
||||
|
||||
if (!$currentSchema) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
$tables = $currentSchema->getTables();
|
||||
|
||||
if (empty($tables)) {
|
||||
if ($oldSchema->getTables() === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$databaseHelper = $this->injectableFactory->create(Helper::class);
|
||||
$connection = $this->helper->getDbalConnection();
|
||||
$pdo = $this->helper->getPDO();
|
||||
|
||||
$connection = $databaseHelper->getDbalConnection();
|
||||
|
||||
$metadataSchema = $this->getMetadataSchema();
|
||||
|
||||
if (!$metadataSchema) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
$tables = $metadataSchema->getTables();
|
||||
|
||||
foreach ($tables as $table) {
|
||||
foreach ($newSchema->getTables() as $table) {
|
||||
$tableName = $table->getName();
|
||||
$indexes = $table->getIndexes();
|
||||
|
||||
@@ -83,26 +68,25 @@ class FulltextIndex extends BaseRebuildActions implements Di\InjectableFactoryAw
|
||||
$columns = $index->getColumns();
|
||||
|
||||
foreach ($columns as $columnName) {
|
||||
|
||||
$query = "SHOW FULL COLUMNS FROM `". $tableName ."` WHERE Field = '" . $columnName . "'";
|
||||
$sql = "SHOW FULL COLUMNS FROM `" . $tableName . "` WHERE Field = " . $pdo->quote($columnName);
|
||||
|
||||
try {
|
||||
/** @var array{Type: string, Collation: string} $row */
|
||||
$row = $connection->fetchAssociative($query);
|
||||
$row = $connection->fetchAssociative($sql);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
catch (Exception) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (strtoupper($row['Type'])) {
|
||||
case 'LONGTEXT':
|
||||
$alterQuery =
|
||||
"ALTER TABLE `". $tableName ."` " .
|
||||
"MODIFY `". $columnName ."` MEDIUMTEXT COLLATE ". $row['Collation'] ."";
|
||||
$alterSql =
|
||||
"ALTER TABLE `{$tableName}` " .
|
||||
"MODIFY `{$columnName}` MEDIUMTEXT COLLATE " . $row['Collation'];
|
||||
|
||||
$this->log->info('SCHEMA, Execute Query: ' . $alterQuery);
|
||||
$this->log->info('SCHEMA, Execute Query: ' . $alterSql);
|
||||
|
||||
$connection->executeQuery($alterQuery);
|
||||
$connection->executeQuery($alterSql);
|
||||
|
||||
break;
|
||||
}
|
||||
+57
-111
@@ -29,84 +29,65 @@
|
||||
|
||||
namespace Espo\Core\Utils\Database\Schema;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Connection as DbalConnection;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema as DBALSchema;
|
||||
use Doctrine\DBAL\Schema\SchemaDiff as DBALSchemaDiff;
|
||||
use Doctrine\DBAL\Schema\Schema as DbalSchema;
|
||||
use Doctrine\DBAL\Schema\SchemaDiff as DbalSchemaDiff;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Database\Converter as DatabaseConverter;
|
||||
use Espo\Core\Utils\Database\DBAL\Schema\Comparator;
|
||||
use Espo\Core\Utils\Database\Helper;
|
||||
use Espo\Core\Utils\File\ClassMap;
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\Metadata\OrmMetadataData;
|
||||
use Espo\Core\Utils\Module\PathProvider;
|
||||
use Espo\Core\Utils\Util;
|
||||
|
||||
use Throwable;
|
||||
|
||||
class Schema
|
||||
/**
|
||||
* A database schema manager.
|
||||
*/
|
||||
class SchemaManager
|
||||
{
|
||||
private string $fieldTypePath = 'application/Espo/Core/Utils/Database/DBAL/FieldTypes';
|
||||
private string $rebuildActionsPath = 'Core/Utils/Database/Schema/rebuildActions';
|
||||
|
||||
private Comparator $comparator;
|
||||
private DatabaseConverter $converter;
|
||||
private Converter $schemaConverter;
|
||||
|
||||
/**
|
||||
* @var ?array{
|
||||
* beforeRebuild: BaseRebuildActions[],
|
||||
* afterRebuild: BaseRebuildActions[],
|
||||
* }
|
||||
*/
|
||||
private $rebuildActions = null;
|
||||
private Builder $builder;
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private Metadata $metadata,
|
||||
private FileManager $fileManager,
|
||||
private ClassMap $classMap,
|
||||
protected OrmMetadataData $ormMetadataData,
|
||||
private OrmMetadataData $ormMetadataData,
|
||||
private Log $log,
|
||||
PathProvider $pathProvider,
|
||||
DatabaseConverter $databaseConverter,
|
||||
private Helper $databaseHelper,
|
||||
private Helper $helper,
|
||||
private MetadataProvider $metadataProvider,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {
|
||||
$this->converter = $databaseConverter;
|
||||
|
||||
$this->comparator = new Comparator();
|
||||
|
||||
$this->initFieldTypes();
|
||||
|
||||
$this->schemaConverter = new Converter(
|
||||
$this->metadata,
|
||||
$this->fileManager,
|
||||
$this,
|
||||
$this->config,
|
||||
$this->log,
|
||||
$pathProvider
|
||||
$this->builder = $this->injectableFactory->createWithBinding(
|
||||
Builder::class,
|
||||
BindingContainerBuilder::create()
|
||||
->bindInstance(Helper::class, $this->helper)
|
||||
->build()
|
||||
);
|
||||
}
|
||||
|
||||
public function getDatabaseHelper(): Helper
|
||||
{
|
||||
return $this->databaseHelper;
|
||||
return $this->helper;
|
||||
}
|
||||
|
||||
public function getPlatform(): AbstractPlatform
|
||||
private function getPlatform(): AbstractPlatform
|
||||
{
|
||||
return $this->getConnection()->getDatabasePlatform();
|
||||
return $this->getDbalConnection()->getDatabasePlatform();
|
||||
}
|
||||
|
||||
public function getConnection(): Connection
|
||||
private function getDbalConnection(): DbalConnection
|
||||
{
|
||||
return $this->getDatabaseHelper()->getDbalConnection();
|
||||
}
|
||||
@@ -123,7 +104,7 @@ class Schema
|
||||
|
||||
$filePath = Util::concatPath($this->fieldTypePath, $typeName . 'Type');
|
||||
|
||||
/** @var class-string<\Doctrine\DBAL\Types\Type> $class */
|
||||
/** @var class-string<Type> $class */
|
||||
$class = Util::getClassName($filePath);
|
||||
|
||||
if (!Type::hasType($dbalTypeName)) {
|
||||
@@ -143,75 +124,71 @@ class Schema
|
||||
$dbTypeName = $dbalTypeName;
|
||||
}
|
||||
|
||||
$this->getConnection()
|
||||
$this->getDbalConnection()
|
||||
->getDatabasePlatform()
|
||||
->registerDoctrineTypeMapping($dbTypeName, $dbalTypeName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild database schema.
|
||||
* Rebuild database schema. Creates and alters needed tables and columns.
|
||||
* Does not remove columns, does not decrease column lengths.
|
||||
*
|
||||
* @param ?string[] $entityList
|
||||
* @param ?string[] $entityTypeList Specific entity types.
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function rebuild(?array $entityList = null): bool
|
||||
public function rebuild(?array $entityTypeList = null): bool
|
||||
{
|
||||
if (!$this->converter->process()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$currentSchema = $this->getCurrentSchema();
|
||||
|
||||
$metadataSchema = $this->schemaConverter->process($this->ormMetadataData->getData(), $entityList);
|
||||
|
||||
$this->initRebuildActions($currentSchema, $metadataSchema);
|
||||
$schema = $this->builder->build($this->ormMetadataData->getData(), $entityTypeList);
|
||||
|
||||
try {
|
||||
$this->executeRebuildActions('beforeRebuild');
|
||||
$this->processPreRebuildActions($currentSchema, $schema);
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
$this->log->alert('Rebuild database fault: '. $e);
|
||||
$this->log->alert('Rebuild database pre-rebuild error: '. $e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$queries = $this->getDiffSql($currentSchema, $metadataSchema);
|
||||
$queries = $this->getDiffSql($currentSchema, $schema);
|
||||
|
||||
$result = true;
|
||||
$connection = $this->getConnection();
|
||||
|
||||
$connection = $this->getDbalConnection();
|
||||
|
||||
foreach ($queries as $sql) {
|
||||
$this->log->info('SCHEMA, Execute Query: '.$sql);
|
||||
$this->log->info('SCHEMA, Execute Query: '. $sql);
|
||||
|
||||
try {
|
||||
$result &= (bool) $connection->executeQuery($sql);
|
||||
$connection->executeQuery($sql);
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
$this->log->alert('Rebuild database fault: '. $e);
|
||||
$this->log->alert('Rebuild database error: ' . $e->getMessage());
|
||||
|
||||
$result = false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$this->executeRebuildActions('afterRebuild');
|
||||
$this->processPostRebuildActions($currentSchema, $schema);
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
$this->log->alert('Rebuild database fault: '. $e);
|
||||
$this->log->alert('Rebuild database post-rebuild error: ' . $e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) $result;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current database schema.
|
||||
*/
|
||||
private function getCurrentSchema(): DBALSchema
|
||||
private function getCurrentSchema(): DbalSchema
|
||||
{
|
||||
return $this->getConnection()
|
||||
return $this->getDbalConnection()
|
||||
->getSchemaManager()
|
||||
->createSchema();
|
||||
}
|
||||
@@ -221,7 +198,7 @@ class Schema
|
||||
*
|
||||
* @return string[] Array of SQL queries.
|
||||
*/
|
||||
public function toSql(DBALSchemaDiff $schema)
|
||||
private function toSql(DbalSchemaDiff $schema)
|
||||
{
|
||||
return $schema->toSaveSql($this->getPlatform());
|
||||
}
|
||||
@@ -232,67 +209,36 @@ class Schema
|
||||
* @return string[] Array of SQL queries.
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function getDiffSql(DBALSchema $fromSchema, DBALSchema $toSchema)
|
||||
private function getDiffSql(DbalSchema $fromSchema, DbalSchema $toSchema)
|
||||
{
|
||||
$schemaDiff = $this->comparator->compare($fromSchema, $toSchema);
|
||||
|
||||
return $this->toSql($schemaDiff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Init Rebuild Actions, get all classes and create them.
|
||||
*/
|
||||
private function initRebuildActions(?DBALSchema $currentSchema = null, ?DBALSchema $metadataSchema = null): void
|
||||
private function processPreRebuildActions(DbalSchema $actualSchema, DbalSchema $schema): void
|
||||
{
|
||||
$methodList = [
|
||||
'beforeRebuild',
|
||||
'afterRebuild',
|
||||
];
|
||||
$binding = BindingContainerBuilder::create()
|
||||
->bindInstance(Helper::class, $this->helper)
|
||||
->build();
|
||||
|
||||
/** @var array<string, class-string<BaseRebuildActions>> $classes */
|
||||
$classes = $this->classMap->getData($this->rebuildActionsPath, null, $methodList);
|
||||
foreach ($this->metadataProvider->getPreRebuildActionClassNameList() as $className) {
|
||||
$action = $this->injectableFactory->createWithBinding($className, $binding);
|
||||
|
||||
$objects = [
|
||||
'beforeRebuild' => [],
|
||||
'afterRebuild' => [],
|
||||
];
|
||||
|
||||
foreach ($classes as $className) {
|
||||
$actionObj = $this->injectableFactory->create($className);
|
||||
|
||||
if (isset($currentSchema)) {
|
||||
$actionObj->setCurrentSchema($currentSchema);
|
||||
}
|
||||
|
||||
if (isset($metadataSchema)) {
|
||||
$actionObj->setMetadataSchema($metadataSchema);
|
||||
}
|
||||
|
||||
foreach ($methodList as $methodName) {
|
||||
if (method_exists($actionObj, $methodName)) {
|
||||
$objects[$methodName][] = $actionObj;
|
||||
}
|
||||
}
|
||||
$action->process($actualSchema, $schema);
|
||||
}
|
||||
|
||||
$this->rebuildActions = $objects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute actions for RebuildAction classes.
|
||||
*
|
||||
* @param 'beforeRebuild'|'afterRebuild' $action An action name.
|
||||
*/
|
||||
private function executeRebuildActions(string $action): void
|
||||
private function processPostRebuildActions(DbalSchema $actualSchema, DbalSchema $schema): void
|
||||
{
|
||||
if (!isset($this->rebuildActions)) {
|
||||
$this->initRebuildActions();
|
||||
}
|
||||
$binding = BindingContainerBuilder::create()
|
||||
->bindInstance(Helper::class, $this->helper)
|
||||
->build();
|
||||
|
||||
assert($this->rebuildActions !== null);
|
||||
foreach ($this->metadataProvider->getPostRebuildActionClassNameList() as $className) {
|
||||
$action = $this->injectableFactory->createWithBinding($className, $binding);
|
||||
|
||||
foreach ($this->rebuildActions[$action] as $rebuildActionClass) {
|
||||
$rebuildActionClass->$action();
|
||||
$action->process($actualSchema, $schema);
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-19
@@ -29,37 +29,31 @@
|
||||
|
||||
namespace Espo\Core\Utils\Database\Schema;
|
||||
|
||||
use Espo\Core\{
|
||||
Container,
|
||||
Utils\Database\Helper,
|
||||
};
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Database\Helper;
|
||||
|
||||
class SchemaProxy
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
|
||||
class SchemaManagerProxy
|
||||
{
|
||||
private Container $container;
|
||||
public function __construct(private InjectableFactory $injectableFactory) {}
|
||||
|
||||
public function __construct(Container $container)
|
||||
private function getSchemaManager(): SchemaManager
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
protected function getSchema(): Schema
|
||||
{
|
||||
/** @var Schema */
|
||||
return $this->container->get('schema');
|
||||
return $this->injectableFactory->create(SchemaManager::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string[] $entityList
|
||||
* @throws \Doctrine\DBAL\Schema\SchemaException
|
||||
* @param ?string[] $entityTypeList
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function rebuild(?array $entityList = null): bool
|
||||
public function rebuild(?array $entityTypeList = null): bool
|
||||
{
|
||||
return $this->getSchema()->rebuild();
|
||||
return $this->getSchemaManager()->rebuild($entityTypeList);
|
||||
}
|
||||
|
||||
public function getDatabaseHelper(): Helper
|
||||
{
|
||||
return $this->getSchema()->getDatabaseHelper();
|
||||
return $this->getSchemaManager()->getDatabaseHelper();
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ class Utils
|
||||
{
|
||||
/**
|
||||
* Get indexes in specific format.
|
||||
* @deprecated
|
||||
*
|
||||
* @param array<string, mixed> $defs
|
||||
* @param string[] $ignoreFlags @todo Remove parameter?
|
||||
@@ -46,52 +47,48 @@ class Utils
|
||||
$indexList = [];
|
||||
|
||||
foreach ($defs as $entityType => $entityParams) {
|
||||
$entityIndexList = self::getEntityIndexListByFieldsDefs($entityParams['fields'] ?? []);
|
||||
$indexes = $entityParams['indexes'] ?? [];
|
||||
|
||||
foreach ($entityIndexList as $indexName => $indexParams) {
|
||||
if (!isset($entityParams['indexes'][$indexName])) {
|
||||
$entityParams['indexes'][$indexName] = $indexParams;
|
||||
foreach ($indexes as $indexName => $indexParams) {
|
||||
$indexDefs = IndexDefs::fromRaw($indexParams, $indexName);
|
||||
|
||||
$tableIndexName = $indexParams['key'] ?? null;
|
||||
|
||||
if (!$tableIndexName) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($entityParams['indexes']) && is_array($entityParams['indexes'])) {
|
||||
foreach ($entityParams['indexes'] as $indexName => $indexParams) {
|
||||
$indexDefs = IndexDefs::fromRaw($indexParams, $indexName);
|
||||
$columns = $indexDefs->getColumnList();
|
||||
$flags = $indexDefs->getFlagList();
|
||||
|
||||
$tableIndexName = $indexParams['key'] ?? self::generateIndexName($indexDefs, $entityType);
|
||||
if ($flags !== []) {
|
||||
$skipIndex = false;
|
||||
|
||||
$columns = $indexDefs->getColumnList();
|
||||
$flags = $indexDefs->getFlagList();
|
||||
foreach ($ignoreFlags as $ignoreFlag) {
|
||||
if (($flagKey = array_search($ignoreFlag, $flags)) !== false) {
|
||||
unset($flags[$flagKey]);
|
||||
|
||||
if ($flags !== []) {
|
||||
$skipIndex = false;
|
||||
|
||||
foreach ($ignoreFlags as $ignoreFlag) {
|
||||
if (($flagKey = array_search($ignoreFlag, $flags)) !== false) {
|
||||
unset($flags[$flagKey]);
|
||||
|
||||
$skipIndex = true;
|
||||
}
|
||||
$skipIndex = true;
|
||||
}
|
||||
|
||||
if ($skipIndex && empty($flags)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$indexList[$entityType][$tableIndexName]['flags'] = $flags;
|
||||
}
|
||||
|
||||
if ($columns !== []) {
|
||||
$indexType = self::getIndexTypeByIndexDefs($indexDefs);
|
||||
|
||||
// @todo Revise, may to be removed.
|
||||
$indexList[$entityType][$tableIndexName]['type'] = $indexType;
|
||||
|
||||
$indexList[$entityType][$tableIndexName]['columns'] = array_map(
|
||||
fn ($item) => Util::toUnderScore($item),
|
||||
$columns
|
||||
);
|
||||
if ($skipIndex && empty($flags)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$indexList[$entityType][$tableIndexName]['flags'] = $flags;
|
||||
}
|
||||
|
||||
if ($columns !== []) {
|
||||
$indexType = self::getIndexTypeByIndexDefs($indexDefs);
|
||||
|
||||
// @todo Revise, may to be removed.
|
||||
$indexList[$entityType][$tableIndexName]['type'] = $indexType;
|
||||
|
||||
$indexList[$entityType][$tableIndexName]['columns'] = array_map(
|
||||
fn ($item) => Util::toUnderScore($item),
|
||||
$columns
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,82 +97,6 @@ class Utils
|
||||
return $indexList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $fieldDefs
|
||||
*/
|
||||
private static function getIndexTypeByFieldDefs(array $fieldDefs): ?string
|
||||
{
|
||||
if ($fieldDefs['type'] != 'id' && isset($fieldDefs['unique']) && $fieldDefs['unique']) {
|
||||
return 'unique';
|
||||
}
|
||||
|
||||
if (isset($fieldDefs['index']) && $fieldDefs['index']) {
|
||||
return 'index';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $fieldDefs
|
||||
*/
|
||||
private static function getIndexNameByFieldDefs(string $fieldName, array $fieldDefs): ?string
|
||||
{
|
||||
$indexType = self::getIndexTypeByFieldDefs($fieldDefs);
|
||||
|
||||
if ($indexType) {
|
||||
$keyValue = $fieldDefs[$indexType];
|
||||
|
||||
if ($keyValue === true) {
|
||||
return $fieldName;
|
||||
}
|
||||
|
||||
if (is_string($keyValue)) {
|
||||
return $keyValue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $fieldsDefs
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function getEntityIndexListByFieldsDefs(array $fieldsDefs, bool $isTableColumnNames = false): array
|
||||
{
|
||||
$indexList = [];
|
||||
|
||||
foreach ($fieldsDefs as $fieldName => $fieldParams) {
|
||||
if (isset($fieldParams['notStorable']) && $fieldParams['notStorable']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$indexType = self::getIndexTypeByFieldDefs($fieldParams);
|
||||
$indexName = self::getIndexNameByFieldDefs($fieldName, $fieldParams);
|
||||
|
||||
if (!$indexType || !$indexName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$keyValue = $fieldParams[$indexType];
|
||||
|
||||
$columnName = $isTableColumnNames ? Util::toUnderScore($fieldName) : $fieldName;
|
||||
|
||||
if ($keyValue === true) {
|
||||
$indexList[$indexName]['type'] = $indexType;
|
||||
$indexList[$indexName]['columns'] = [$columnName];
|
||||
}
|
||||
else if (is_string($keyValue)) {
|
||||
$indexList[$indexName]['type'] = $indexType;
|
||||
$indexList[$indexName]['columns'][] = $columnName;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array<string,mixed> */
|
||||
return $indexList;
|
||||
}
|
||||
|
||||
private static function getIndexTypeByIndexDefs(IndexDefs $indexDefs): string
|
||||
{
|
||||
if ($indexDefs->isUnique()) {
|
||||
@@ -189,23 +110,6 @@ class Utils
|
||||
return 'index';
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Move to IndexHelper interface.
|
||||
*/
|
||||
public static function generateIndexName(IndexDefs $defs, string $entityType): string
|
||||
{
|
||||
$maxLength = 60;
|
||||
|
||||
$name = $defs->getName();
|
||||
$prefix = $defs->isUnique() ? 'UNIQ' : 'IDX';
|
||||
|
||||
$parts = [$prefix, strtoupper(Util::toUnderScore($name))];
|
||||
|
||||
$key = implode('_', $parts);
|
||||
|
||||
return substr($key, 0, $maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
|
||||
@@ -31,42 +31,28 @@ namespace Espo\Core\Utils\Metadata;
|
||||
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Database\Converter;
|
||||
use Espo\Core\Utils\Database\Orm\Converter;
|
||||
use Espo\Core\Utils\DataCache;
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\Util;
|
||||
|
||||
class OrmMetadataData
|
||||
{
|
||||
/** @var ?array<string, array<string, mixed>> */
|
||||
protected $data = null;
|
||||
|
||||
protected string $cacheKey = 'ormMetadata';
|
||||
|
||||
protected bool $useCache;
|
||||
protected Metadata $metadata;
|
||||
protected FileManager $fileManager;
|
||||
protected DataCache $dataCache;
|
||||
protected Config $config;
|
||||
private $data = null;
|
||||
private string $cacheKey = 'ormMetadata';
|
||||
private bool $useCache;
|
||||
private ?Converter $converter = null;
|
||||
|
||||
public function __construct(
|
||||
Metadata $metadata,
|
||||
FileManager $fileManager,
|
||||
DataCache $dataCache,
|
||||
Config $config,
|
||||
private DataCache $dataCache,
|
||||
private Config $config,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {
|
||||
$this->metadata = $metadata;
|
||||
$this->fileManager = $fileManager;
|
||||
$this->dataCache = $dataCache;
|
||||
$this->config = $config;
|
||||
|
||||
$this->useCache = (bool) $this->config->get('useCache', false);
|
||||
}
|
||||
|
||||
protected function getConverter(): Converter
|
||||
private function getConverter(): Converter
|
||||
{
|
||||
if (!isset($this->converter)) {
|
||||
$this->converter = $this->injectableFactory->create(Converter::class);
|
||||
@@ -76,9 +62,27 @@ class OrmMetadataData
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads data.
|
||||
*/
|
||||
public function reload(): void
|
||||
{
|
||||
$this->getDataInternal(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw data.
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public function getData(bool $reload = false): array
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->getDataInternal();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function getDataInternal(bool $reload = false): array
|
||||
{
|
||||
if (isset($this->data) && !$reload) {
|
||||
return $this->data;
|
||||
|
||||
@@ -29,25 +29,25 @@
|
||||
|
||||
namespace Espo\Core\Utils;
|
||||
|
||||
use Espo\Core\ORM\DatabaseParamsFactory;
|
||||
use Espo\Core\Utils\Database\Helper as DatabaseHelper;
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
|
||||
use PDO;
|
||||
|
||||
class SystemRequirements
|
||||
{
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private FileManager $fileManager,
|
||||
private System $systemHelper,
|
||||
private DatabaseHelper $databaseHelper
|
||||
private DatabaseHelper $databaseHelper,
|
||||
private DatabaseParamsFactory $databaseParamsFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* php: array<string,array<string,mixed>>,
|
||||
* database: array<string,array<string,mixed>>,
|
||||
* permission: array<string,array<string,mixed>>,
|
||||
* php: array<string, array<string, mixed>>,
|
||||
* database: array<string, array<string, mixed>>,
|
||||
* permission: array<string, array<string, mixed>>,
|
||||
* }
|
||||
*/
|
||||
public function getAllRequiredList(bool $requiredOnly = false): array
|
||||
@@ -55,16 +55,16 @@ class SystemRequirements
|
||||
return [
|
||||
'php' => $this->getPhpRequiredList($requiredOnly),
|
||||
'database' => $this->getDatabaseRequiredList($requiredOnly),
|
||||
'permission' => $this->getRequiredPermissionList($requiredOnly),
|
||||
'permission' => $this->getRequiredPermissionList(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $additionalData
|
||||
* @param array<string, mixed> $additionalData
|
||||
* @return array{
|
||||
* php?: array<string,array<string,mixed>>,
|
||||
* database?: array<string,array<string,mixed>>,
|
||||
* permission?: array<string,array{type:string,acceptable:int}>,
|
||||
* php?: array<string, array<string, mixed>>,
|
||||
* database?: array<string, array<string, mixed>>,
|
||||
* permission?: array<string, array{type: string, acceptable: int}>,
|
||||
* }
|
||||
*/
|
||||
public function getRequiredListByType(
|
||||
@@ -73,27 +73,20 @@ class SystemRequirements
|
||||
array $additionalData = null
|
||||
): array {
|
||||
|
||||
switch ($type) {
|
||||
case 'php':
|
||||
return $this->getPhpRequiredList($requiredOnly, $additionalData);
|
||||
|
||||
case 'database':
|
||||
return $this->getDatabaseRequiredList($requiredOnly, $additionalData);
|
||||
|
||||
case 'permission':
|
||||
return $this->getRequiredPermissionList($requiredOnly, $additionalData);
|
||||
}
|
||||
|
||||
return [];
|
||||
return match ($type) {
|
||||
'php' => $this->getPhpRequiredList($requiredOnly),
|
||||
'database' => $this->getDatabaseRequiredList($requiredOnly, $additionalData),
|
||||
'permission' => $this->getRequiredPermissionList(),
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get required PHP params.
|
||||
*
|
||||
* @param ?array<string,mixed> $additionalData
|
||||
* @return array<string,array<string,mixed>>
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
protected function getPhpRequiredList(bool $requiredOnly, ?array $additionalData = null): array
|
||||
public function getPhpRequiredList(bool $requiredOnly): array
|
||||
{
|
||||
$requiredList = [
|
||||
'requiredPhpVersion',
|
||||
@@ -107,26 +100,25 @@ class SystemRequirements
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->getRequiredList('phpRequirements', $requiredList, $additionalData);
|
||||
return $this->getRequiredList('phpRequirements', $requiredList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get required DB params.
|
||||
*
|
||||
* @param ?array<string,mixed> $additionalData
|
||||
* @return array<string,array<string,mixed>>
|
||||
* @param ?array<string, mixed> $additionalData
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
protected function getDatabaseRequiredList(bool $requiredOnly, ?array $additionalData = null): array
|
||||
private function getDatabaseRequiredList(bool $requiredOnly, ?array $additionalData = null): array
|
||||
{
|
||||
$databaseHelper = $this->databaseHelper;
|
||||
$databaseParams = $additionalData['database'] ?? [];
|
||||
$databaseParams = $this->databaseParamsFactory
|
||||
->createWithMergedAssoc($additionalData['databaseParams'] ?? []);
|
||||
|
||||
$pdoConnection = $databaseHelper->createPdoConnection($databaseParams);
|
||||
$pdo = $this->databaseHelper->createPDO($databaseParams);
|
||||
|
||||
$databaseHelper->setPdoConnection($pdoConnection);
|
||||
$databaseType = $databaseHelper->getDatabaseType();
|
||||
$databaseTypeName = ucfirst(strtolower($databaseType));
|
||||
$this->databaseHelper = $this->databaseHelper->withPDO($pdo);
|
||||
|
||||
$databaseTypeName = ucfirst(strtolower($this->databaseHelper->getType()));
|
||||
|
||||
$requiredList = [
|
||||
'required' . $databaseTypeName . 'Version',
|
||||
@@ -145,17 +137,14 @@ class SystemRequirements
|
||||
/**
|
||||
* Get permission requirements.
|
||||
*
|
||||
* @param ?array<string,mixed> $additionalData
|
||||
* @return array<string,array<string,mixed>>
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function getRequiredPermissionList(bool $requiredOnly, ?array $additionalData = null): array
|
||||
private function getRequiredPermissionList(): array
|
||||
{
|
||||
return $this->getRequiredList(
|
||||
'permissionRequirements',
|
||||
[
|
||||
'permissionMap.writable',
|
||||
],
|
||||
$additionalData,
|
||||
['permissionMap.writable'],
|
||||
null,
|
||||
[
|
||||
'permissionMap.writable' => $this->fileManager->getPermissionUtils()->getWritableList(),
|
||||
]
|
||||
@@ -164,9 +153,9 @@ class SystemRequirements
|
||||
|
||||
/**
|
||||
* @param string[] $checkList
|
||||
* @param ?array<string,mixed> $additionalData
|
||||
* @param array<string,mixed> $predefinedData
|
||||
* @return array<string,array<string,mixed>>
|
||||
* @param ?array<string, mixed> $additionalData
|
||||
* @param array<string, mixed> $predefinedData
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function getRequiredList(
|
||||
string $type,
|
||||
@@ -178,30 +167,37 @@ class SystemRequirements
|
||||
$list = [];
|
||||
|
||||
foreach ($checkList as $itemName) {
|
||||
$methodName = 'check' . ucfirst($type);
|
||||
$type = lcfirst($type);
|
||||
|
||||
if (method_exists($this, $methodName)) {
|
||||
$itemValue =
|
||||
isset($predefinedData[$itemName]) ?
|
||||
$predefinedData[$itemName] :
|
||||
$this->config->get($itemName);
|
||||
$itemValue = $predefinedData[$itemName] ?? $this->config->get($itemName);
|
||||
|
||||
$result = $this->$methodName($itemName, $itemValue, $additionalData);
|
||||
$list = array_merge($list, $result);
|
||||
$result = [];
|
||||
|
||||
if ($type === 'phpRequirements') {
|
||||
$result = $this->checkPhpRequirements($itemName, $itemValue);
|
||||
}
|
||||
|
||||
if ($type === 'databaseRequirements') {
|
||||
$result = $this->checkDatabaseRequirements($itemName, $itemValue, $additionalData);
|
||||
}
|
||||
|
||||
if ($type === 'permissionRequirements') {
|
||||
$result = $this->checkPermissionRequirements($itemName, $itemValue);
|
||||
}
|
||||
|
||||
$list = array_merge($list, $result);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check PHP requirements,
|
||||
* Check PHP requirements.
|
||||
*
|
||||
* @param array<string,mixed>|string $data
|
||||
* @param ?array<string,mixed> $additionalData
|
||||
* @return array<string,array<string,mixed>>
|
||||
* @param array<string, mixed>|string $data
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function checkPhpRequirements(string $type, $data, ?array $additionalData = null): array
|
||||
private function checkPhpRequirements(string $type, $data): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
@@ -247,9 +243,7 @@ class SystemRequirements
|
||||
$requiredValue = $value;
|
||||
$actualValue = $this->systemHelper->getPhpParam($name) ?: '0';
|
||||
|
||||
$acceptable = (
|
||||
Util::convertToByte($actualValue) >= Util::convertToByte($requiredValue)
|
||||
) ? true : false;
|
||||
$acceptable = Util::convertToByte($actualValue) >= Util::convertToByte($requiredValue);
|
||||
|
||||
$list[$name] = [
|
||||
'type' => 'param',
|
||||
@@ -268,9 +262,9 @@ class SystemRequirements
|
||||
/**
|
||||
* Check DB requirements.
|
||||
*
|
||||
* @param array<string,mixed>|string $data
|
||||
* @param ?array<string,mixed> $additionalData
|
||||
* @return array<string,array<string,mixed>>
|
||||
* @param array<string, mixed>|string $data
|
||||
* @param ?array<string, mixed> $additionalData
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function checkDatabaseRequirements(string $type, $data, ?array $additionalData = null): array
|
||||
{
|
||||
@@ -278,14 +272,14 @@ class SystemRequirements
|
||||
|
||||
$databaseHelper = $this->databaseHelper;
|
||||
|
||||
$databaseParams = $additionalData['database'] ?? [];
|
||||
$databaseParams = $additionalData['databaseParams'] ?? [];
|
||||
|
||||
switch ($type) {
|
||||
case 'requiredMysqlVersion':
|
||||
case 'requiredMariadbVersion':
|
||||
/** @var string $data */
|
||||
|
||||
$actualVersion = $databaseHelper->getDatabaseServerVersion();
|
||||
$actualVersion = $databaseHelper->getServerVersion();
|
||||
|
||||
$requiredVersion = $data;
|
||||
|
||||
@@ -309,7 +303,7 @@ class SystemRequirements
|
||||
foreach ($data as $name => $value) {
|
||||
$requiredValue = $value;
|
||||
|
||||
$actualValue = $databaseHelper->getDatabaseParam($name);
|
||||
$actualValue = $databaseHelper->getParam($name);
|
||||
|
||||
$acceptable = false;
|
||||
|
||||
@@ -344,21 +338,21 @@ class SystemRequirements
|
||||
$databaseParams = $this->config->get('database');
|
||||
}
|
||||
|
||||
$acceptable = true;
|
||||
|
||||
$list['host'] = [
|
||||
'type' => 'connection',
|
||||
'acceptable' => $acceptable,
|
||||
'acceptable' => true,
|
||||
'actual' => $databaseParams['host'],
|
||||
];
|
||||
|
||||
$list['dbname'] = [
|
||||
'type' => 'connection',
|
||||
'acceptable' => $acceptable,
|
||||
'acceptable' => true,
|
||||
'actual' => $databaseParams['dbname'],
|
||||
];
|
||||
|
||||
$list['user'] = [
|
||||
'type' => 'connection',
|
||||
'acceptable' => $acceptable,
|
||||
'acceptable' => true,
|
||||
'actual' => $databaseParams['user'],
|
||||
];
|
||||
|
||||
@@ -369,11 +363,10 @@ class SystemRequirements
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $data
|
||||
* @param ?array<string,mixed> $additionalData
|
||||
* @return array<string,array<string,mixed>>
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function checkPermissionRequirements(string $type, $data, ?array $additionalData = null): array
|
||||
private function checkPermissionRequirements(string $type, $data): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
@@ -383,21 +376,25 @@ class SystemRequirements
|
||||
case 'permissionMap.writable':
|
||||
foreach ($data as $item) {
|
||||
$fullPathItem = Util::concatPath($this->systemHelper->getRootDir(), $item);
|
||||
|
||||
$list[$fullPathItem] = [
|
||||
'type' => 'writable',
|
||||
'acceptable' => $fileManager->isWritable($fullPathItem) ? true : false,
|
||||
'acceptable' => $fileManager->isWritable($fullPathItem),
|
||||
];
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'permissionMap.readable':
|
||||
foreach ($data as $item) {
|
||||
$fullPathItem = Util::concatPath($this->systemHelper->getRootDir(), $item);
|
||||
|
||||
$list[$fullPathItem] = [
|
||||
'type' => 'readable',
|
||||
'acceptable' => $fileManager->isReadable($fullPathItem) ? true : false,
|
||||
'acceptable' => $fileManager->isReadable($fullPathItem),
|
||||
];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ class Service
|
||||
'parentId',
|
||||
'status',
|
||||
'createdAt',
|
||||
['""', 'hasAttachment'],
|
||||
['null', 'hasAttachment'],
|
||||
])
|
||||
->leftJoin(
|
||||
'MeetingUser',
|
||||
@@ -217,11 +217,11 @@ class Service
|
||||
['dateEnd', 'dateEnd'],
|
||||
(
|
||||
$seed->hasAttribute('dateStartDate') ?
|
||||
['dateStartDate', 'dateStartDate'] : ['""', 'dateStartDate']
|
||||
['dateStartDate', 'dateStartDate'] : ['null', 'dateStartDate']
|
||||
),
|
||||
(
|
||||
$seed->hasAttribute('dateEndDate') ?
|
||||
['dateEndDate', 'dateEndDate'] : ['""', 'dateEndDate']
|
||||
['dateEndDate', 'dateEndDate'] : ['null', 'dateEndDate']
|
||||
),
|
||||
['"Call"', '_scope'],
|
||||
'assignedUserId',
|
||||
@@ -230,7 +230,7 @@ class Service
|
||||
'parentId',
|
||||
'status',
|
||||
'createdAt',
|
||||
['""', 'hasAttachment'],
|
||||
['null', 'hasAttachment'],
|
||||
])
|
||||
->leftJoin(
|
||||
'CallUser',
|
||||
@@ -290,9 +290,9 @@ class Service
|
||||
'id',
|
||||
'name',
|
||||
['dateSent', 'dateStart'],
|
||||
['""', 'dateEnd'],
|
||||
['""', 'dateStartDate'],
|
||||
['""', 'dateEndDate'],
|
||||
['null', 'dateEnd'],
|
||||
['null', 'dateStartDate'],
|
||||
['null', 'dateEndDate'],
|
||||
['"Email"', '_scope'],
|
||||
'assignedUserId',
|
||||
'assignedUserName',
|
||||
@@ -356,11 +356,11 @@ class Service
|
||||
['dateEnd', 'dateEnd'],
|
||||
(
|
||||
$seed->hasAttribute('dateStartDate') ?
|
||||
['dateStartDate', 'dateStartDate'] : ['""', 'dateStartDate']
|
||||
['dateStartDate', 'dateStartDate'] : ['null', 'dateStartDate']
|
||||
),
|
||||
(
|
||||
$seed->hasAttribute('dateEndDate') ?
|
||||
['dateEndDate', 'dateEndDate'] : ['""', 'dateEndDate']
|
||||
['dateEndDate', 'dateEndDate'] : ['null', 'dateEndDate']
|
||||
),
|
||||
['"' . $targetEntityType . '"', '_scope'],
|
||||
'assignedUserId',
|
||||
@@ -369,7 +369,7 @@ class Service
|
||||
'parentId',
|
||||
'status',
|
||||
'createdAt',
|
||||
['""', 'hasAttachment'],
|
||||
['false', 'hasAttachment'],
|
||||
]);
|
||||
|
||||
if (!empty($statusList)) {
|
||||
@@ -501,9 +501,9 @@ class Service
|
||||
'id',
|
||||
'name',
|
||||
['dateSent', 'dateStart'],
|
||||
['""', 'dateEnd'],
|
||||
['""', 'dateStartDate'],
|
||||
['""', 'dateEndDate'],
|
||||
['null', 'dateEnd'],
|
||||
['null', 'dateStartDate'],
|
||||
['null', 'dateEndDate'],
|
||||
['"Email"', '_scope'],
|
||||
'assignedUserId',
|
||||
'assignedUserName',
|
||||
@@ -1091,22 +1091,22 @@ class Service
|
||||
->select([
|
||||
'id',
|
||||
'name',
|
||||
($seed->hasAttribute('dateStart') ? ['dateStart', 'dateStart'] : ['""', 'dateStart']),
|
||||
($seed->hasAttribute('dateEnd') ? ['dateEnd', 'dateEnd'] : ['""', 'dateEnd']),
|
||||
($seed->hasAttribute('dateStart') ? ['dateStart', 'dateStart'] : ['null', 'dateStart']),
|
||||
($seed->hasAttribute('dateEnd') ? ['dateEnd', 'dateEnd'] : ['null', 'dateEnd']),
|
||||
($seed->hasAttribute('dateStartDate') ?
|
||||
['dateStartDate', 'dateStartDate'] : ['""', 'dateStartDate']),
|
||||
['dateStartDate', 'dateStartDate'] : ['null', 'dateStartDate']),
|
||||
($seed->hasAttribute('dateEndDate') ?
|
||||
['dateEndDate', 'dateEndDate'] : ['""', 'dateEndDate']),
|
||||
['dateEndDate', 'dateEndDate'] : ['null', 'dateEndDate']),
|
||||
['"' . $scope . '"', '_scope'],
|
||||
($seed->hasAttribute('assignedUserId') ?
|
||||
['assignedUserId', 'assignedUserId'] : ['""', 'assignedUserId']),
|
||||
['assignedUserId', 'assignedUserId'] : ['null', 'assignedUserId']),
|
||||
($seed->hasAttribute('assignedUserName') ? ['assignedUserName', 'assignedUserName'] :
|
||||
['""', 'assignedUserName']),
|
||||
($seed->hasAttribute('parentType') ? ['parentType', 'parentType'] : ['""', 'parentType']),
|
||||
($seed->hasAttribute('parentId') ? ['parentId', 'parentId'] : ['""', 'parentId']),
|
||||
['null', 'assignedUserName']),
|
||||
($seed->hasAttribute('parentType') ? ['parentType', 'parentType'] : ['null', 'parentType']),
|
||||
($seed->hasAttribute('parentId') ? ['parentId', 'parentId'] : ['null', 'parentId']),
|
||||
'status',
|
||||
'createdAt',
|
||||
['""', 'hasAttachment'],
|
||||
['false', 'hasAttachment'],
|
||||
]);
|
||||
|
||||
if ($entity->getEntityType() === User::ENTITY_TYPE) {
|
||||
|
||||
@@ -258,11 +258,11 @@ class Service
|
||||
'name',
|
||||
['dateStart', 'dateStart'],
|
||||
['dateEnd', 'dateEnd'],
|
||||
($seed->hasAttribute('status') ? ['status', 'status'] : ['""', 'status']),
|
||||
($seed->hasAttribute('dateStartDate') ? ['dateStartDate', 'dateStartDate'] : ['""', 'dateStartDate']),
|
||||
($seed->hasAttribute('dateEndDate') ? ['dateEndDate', 'dateEndDate'] : ['""', 'dateEndDate']),
|
||||
($seed->hasAttribute('parentType') ? ['parentType', 'parentType'] : ['""', 'parentType']),
|
||||
($seed->hasAttribute('parentId') ? ['parentId', 'parentId'] : ['""', 'parentId']),
|
||||
($seed->hasAttribute('status') ? ['status', 'status'] : ['null', 'status']),
|
||||
($seed->hasAttribute('dateStartDate') ? ['dateStartDate', 'dateStartDate'] : ['null', 'dateStartDate']),
|
||||
($seed->hasAttribute('dateEndDate') ? ['dateEndDate', 'dateEndDate'] : ['null', 'dateEndDate']),
|
||||
($seed->hasAttribute('parentType') ? ['parentType', 'parentType'] : ['null', 'parentType']),
|
||||
($seed->hasAttribute('parentId') ? ['parentId', 'parentId'] : ['null', 'parentId']),
|
||||
'createdAt',
|
||||
];
|
||||
|
||||
@@ -273,7 +273,7 @@ class Service
|
||||
foreach ($additionalAttributeList as $attribute) {
|
||||
$select[] = $seed->hasAttribute($attribute) ?
|
||||
[$attribute, $attribute] :
|
||||
['""', $attribute];
|
||||
['null', $attribute];
|
||||
}
|
||||
|
||||
$orGroup = [
|
||||
@@ -369,7 +369,7 @@ class Service
|
||||
foreach ($additionalAttributeList as $attribute) {
|
||||
$select[] = $seed->hasAttribute($attribute) ?
|
||||
[$attribute, $attribute] :
|
||||
['""', $attribute];
|
||||
['null', $attribute];
|
||||
}
|
||||
|
||||
return $builder
|
||||
@@ -414,8 +414,8 @@ class Service
|
||||
['dateStart', 'dateStart'],
|
||||
['dateEnd', 'dateEnd'],
|
||||
'status',
|
||||
['""', 'dateStartDate'],
|
||||
['""', 'dateEndDate'],
|
||||
['null', 'dateStartDate'],
|
||||
['null', 'dateEndDate'],
|
||||
'parentType',
|
||||
'parentId',
|
||||
'createdAt',
|
||||
@@ -430,7 +430,7 @@ class Service
|
||||
foreach ($additionalAttributeList as $attribute) {
|
||||
$select[] = $seed->hasAttribute($attribute) ?
|
||||
[$attribute, $attribute] :
|
||||
['""', $attribute];
|
||||
['null', $attribute];
|
||||
}
|
||||
|
||||
return $builder
|
||||
@@ -491,7 +491,7 @@ class Service
|
||||
foreach ($additionalAttributeList as $attribute) {
|
||||
$select[] = $seed->hasAttribute($attribute) ?
|
||||
[$attribute, $attribute] :
|
||||
['""', $attribute];
|
||||
['null', $attribute];
|
||||
}
|
||||
|
||||
$queryBuilder = $builder
|
||||
|
||||
@@ -293,6 +293,27 @@ class RelationDefs
|
||||
return $this->data['relationName'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get indexes.
|
||||
*
|
||||
* @return IndexDefs[]
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function getIndexList(): array
|
||||
{
|
||||
if ($this->getType() !== Entity::MANY_MANY) {
|
||||
throw new RuntimeException("Can't get indexes.");
|
||||
}
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach (($this->data['indexes'] ?? []) as $name => $item) {
|
||||
$list[] = IndexDefs::fromRaw($item, $name);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a parameter is set.
|
||||
*/
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
"authTokenManager": {
|
||||
"className": "Espo\\Core\\Authentication\\AuthToken\\EspoManager"
|
||||
},
|
||||
"schema": {
|
||||
"className": "Espo\\Core\\Utils\\Database\\Schema\\Schema"
|
||||
},
|
||||
"ormMetadataData": {
|
||||
"className": "Espo\\Core\\Utils\\Metadata\\OrmMetadataData"
|
||||
},
|
||||
@@ -101,4 +98,4 @@
|
||||
"user": {
|
||||
"settable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
{
|
||||
"dbalConnectionFactoryClassNameMap": {
|
||||
"Mysql": "Espo\\Core\\Utils\\Database\\DBAL\\Factories\\MysqlConnectionFactory",
|
||||
"Postgresql": "Espo\\Core\\Utils\\Database\\DBAL\\Factories\\PostgresqlConnectionFactory"
|
||||
"platforms": {
|
||||
"Mysql": {
|
||||
"detailsProviderClassName": "Espo\\Core\\Utils\\Database\\DetailsProviders\\MysqlDetailsProvider",
|
||||
"dbalConnectionFactoryClassName": "Espo\\Core\\Utils\\Database\\DBAL\\Factories\\MysqlConnectionFactory",
|
||||
"indexHelperClassName": "Espo\\Core\\Utils\\Database\\Orm\\IndexHelpers\\MysqlIndexHelper",
|
||||
"columnPreparatorClassName": "Espo\\Core\\Utils\\Database\\Schema\\ColumnPreparators\\MysqlColumnPreparator",
|
||||
"preRebuildActionClassNameList": [
|
||||
"Espo\\Core\\Utils\\Database\\Schema\\RebuildActions\\PrepareForFulltextIndex"
|
||||
],
|
||||
"postRebuildActionClassNameList": []
|
||||
},
|
||||
"Postgresql": {
|
||||
"dbalConnectionFactoryClassName": "Espo\\Core\\Utils\\Database\\DBAL\\Factories\\PostgresqlConnectionFactory"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"actionClassNameList": [
|
||||
"Espo\\Core\\Rebuild\\Actions\\AddSystemUser",
|
||||
"Espo\\Core\\Rebuild\\Actions\\CurrencyRates",
|
||||
"Espo\\Core\\Rebuild\\Actions\\ScheduledJobs",
|
||||
"Espo\\Core\\Rebuild\\Actions\\ConfigMetadataCheck"
|
||||
]
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Services;
|
||||
|
||||
use Espo\Core\Utils\Database\Schema\Utils as SchemaUtils;
|
||||
|
||||
/**
|
||||
* @todo Remove.
|
||||
* @deprecated
|
||||
*/
|
||||
class MysqlCharacter extends \Espo\Core\Services\Base
|
||||
{
|
||||
protected function init() /** @phpstan-ignore-line */
|
||||
{
|
||||
$this->addDependency('container');
|
||||
}
|
||||
|
||||
protected function getContainer() /** @phpstan-ignore-line */
|
||||
{
|
||||
return $this->getInjection('container');
|
||||
}
|
||||
|
||||
public function jobConvertToMb4() /** @phpstan-ignore-line */
|
||||
{
|
||||
$container = $this->getContainer();
|
||||
|
||||
|
||||
/* @var $em \Espo\ORM\EntityManager */
|
||||
$em = $container->get('entityManager');
|
||||
|
||||
$sqlExecutor = $em->getSqlExecutor();
|
||||
|
||||
$ormMeta = $container->get('ormMetadata')->getData(true);
|
||||
|
||||
$databaseSchema = $container->get('schema');
|
||||
$maxIndexLength = $databaseSchema->getDatabaseHelper()->getMaxIndexLength();
|
||||
|
||||
if ($maxIndexLength > 1000) {
|
||||
$maxIndexLength = 1000;
|
||||
}
|
||||
|
||||
$sth = $sqlExecutor->execute("SELECT `name` FROM `account` WHERE LENGTH(name) > 249");
|
||||
|
||||
if (!$sth->fetch()) {
|
||||
$sqlExecutor->execute("ALTER TABLE `account` MODIFY `name` VARCHAR(249)");
|
||||
}
|
||||
|
||||
$fieldListExceededIndexMaxLength = SchemaUtils::getFieldListExceededIndexMaxLength($ormMeta, $maxIndexLength);
|
||||
|
||||
foreach ($ormMeta as $entityName => $entityParams) {
|
||||
$tableName = \Espo\Core\Utils\Util::toUnderScore($entityName);
|
||||
|
||||
// Get table columns params
|
||||
/** @phpstan-ignore-next-line */
|
||||
$query = "SHOW FULL COLUMNS FROM `". $tableName ."` WHERE `Collation` <> 'utf8mb4_unicode_ci'";
|
||||
|
||||
try {
|
||||
$sth = $sqlExecutor->execute($query);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$GLOBALS['log']->debug('Utf8mb4: Table does not exist - ' . $e->getMessage());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$columnParams = [];
|
||||
|
||||
$rowList = $sth->fetchAll();
|
||||
|
||||
foreach ($rowList as $row) {
|
||||
$columnParams[ $row['Field'] ] = $row;
|
||||
}
|
||||
// END: get table columns params
|
||||
|
||||
foreach ($entityParams['fields'] as $fieldName => $fieldParams) {
|
||||
$columnName = \Espo\Core\Utils\Util::toUnderScore($fieldName);
|
||||
|
||||
if (isset($fieldParams['notStorable']) && $fieldParams['notStorable']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($fieldListExceededIndexMaxLength[$entityName]) && in_array($fieldName, $fieldListExceededIndexMaxLength[$entityName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
if (!isset($columnParams[$columnName]) || empty($columnParams[$columnName]['Type'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$query = null;
|
||||
|
||||
switch ($fieldParams['type']) {
|
||||
case 'varchar':
|
||||
case 'text':
|
||||
case 'jsonObject':
|
||||
case 'jsonArray':
|
||||
/** @phpstan-ignore-next-line */
|
||||
$query = "ALTER TABLE `".$tableName."` ".
|
||||
/** @phpstan-ignore-next-line */
|
||||
"CHANGE COLUMN `". $columnName ."` `". $columnName ."` ". $columnParams[$columnName]['Type'] .
|
||||
/** @phpstan-ignore-next-line */
|
||||
" CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;";
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!empty($query)) {
|
||||
$GLOBALS['log']->debug('Utf8mb4: execute the query - [' . $query . '].');
|
||||
|
||||
try {
|
||||
$sqlExecutor->execute($query);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$GLOBALS['log']->warning(
|
||||
'Utf8mb4: FAILED executing the query - [' . $query . '], details: '. $e->getMessage() .'.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$config = $container->get('config');
|
||||
$database = $config->get('database');
|
||||
|
||||
if (!isset($database['charset']) || $database['charset'] != 'utf8mb4') {
|
||||
$database['charset'] = 'utf8mb4';
|
||||
$config->set('database', $database);
|
||||
$config->save();
|
||||
}
|
||||
|
||||
$this->getContainer()->get('dataManager')->rebuild();
|
||||
}
|
||||
}
|
||||
@@ -30,13 +30,11 @@
|
||||
namespace Espo\Tools\Currency;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\DataManager;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Config\ConfigWriter;
|
||||
|
||||
use Espo\Core\Utils\Currency\DatabasePopulator;
|
||||
use stdClass;
|
||||
|
||||
class RateService
|
||||
@@ -46,8 +44,8 @@ class RateService
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private ConfigWriter $configWriter,
|
||||
private DataManager $dataManager,
|
||||
private Acl $acl
|
||||
private Acl $acl,
|
||||
private DatabasePopulator $databasePopulator
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -71,7 +69,6 @@ class RateService
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
*/
|
||||
public function set(stdClass $rates): stdClass
|
||||
{
|
||||
@@ -114,12 +111,10 @@ class RateService
|
||||
}
|
||||
|
||||
$this->configWriter->set('currencyRates', $rates);
|
||||
|
||||
$this->configWriter->save();
|
||||
$this->dataManager->rebuildDatabase([]);
|
||||
|
||||
return (object) (
|
||||
$config->get('currencyRates') ?? []
|
||||
);
|
||||
$this->databasePopulator->process();
|
||||
|
||||
return (object) ($config->get('currencyRates') ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ use Espo\Core\Application;
|
||||
use Espo\Core\Container;
|
||||
use Espo\Core\InjectableFactory;
|
||||
|
||||
use Espo\Core\ORM\DatabaseParamsFactory;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\Core\Utils\Config\ConfigFileManager;
|
||||
use Espo\Core\Utils\Config;
|
||||
@@ -57,6 +58,7 @@ class Installer
|
||||
private $isAuth = false;
|
||||
private $passwordHash;
|
||||
private $defaultSettings;
|
||||
private DatabaseParamsFactory $databaseParamsFactory;
|
||||
|
||||
private $permittedSettingList = [
|
||||
'dateFormat',
|
||||
@@ -92,6 +94,7 @@ class Installer
|
||||
$this->systemHelper = new SystemHelper();
|
||||
|
||||
$this->databaseHelper = $this->getInjectableFactory()->create(DatabaseHelper::class);
|
||||
$this->databaseParamsFactory = $this->getInjectableFactory()->create(DatabaseParamsFactory::class);
|
||||
}
|
||||
|
||||
private function initialize(): void
|
||||
@@ -297,6 +300,7 @@ class Installer
|
||||
|
||||
public function getSystemRequirementList($type, $requiredOnly = false, array $additionalData = null)
|
||||
{
|
||||
/** @var SystemRequirements $systemRequirementManager */
|
||||
$systemRequirementManager = $this->app
|
||||
->getContainer()
|
||||
->get('injectableFactory')
|
||||
@@ -309,13 +313,14 @@ class Installer
|
||||
array $params,
|
||||
bool $createDatabase = false
|
||||
) {
|
||||
$databaseParams = $this->databaseParamsFactory->createWithMergedAssoc($params);
|
||||
|
||||
try {
|
||||
$pdo = $this->getDatabaseHelper()->createPdoConnection($params);
|
||||
$this->getDatabaseHelper()->createPDO($databaseParams);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
if ($createDatabase && $e->getCode() == '1049') {
|
||||
$pdo = $this->getDatabaseHelper()
|
||||
->createPdoConnection($params, true);
|
||||
$pdo = $this->getDatabaseHelper()->createPDO($databaseParams, true);
|
||||
|
||||
$dbname = preg_replace('/[^A-Za-z0-9_\-@$#\(\)]+/', '', $params['dbname']);
|
||||
|
||||
|
||||
@@ -83,7 +83,8 @@ if ($result['success'] && !empty($allPostData['dbName']) && !empty($allPostData[
|
||||
}
|
||||
|
||||
if ($isConnected) {
|
||||
$databaseRequiredList = $installer->getSystemRequirementList('database', true, ['database' => $databaseParams]);
|
||||
$databaseRequiredList = $installer
|
||||
->getSystemRequirementList('database', true, ['databaseParams' => $databaseParams]);
|
||||
|
||||
foreach ($databaseRequiredList as $name => $details) {
|
||||
if (!$details['acceptable']) {
|
||||
|
||||
@@ -33,15 +33,15 @@ $smarty->assign('phpRequirementList', $phpRequirementList);
|
||||
$installData = $_SESSION['install'];
|
||||
$hostData = explode(':', $installData['host-name']);
|
||||
|
||||
$dbConfig = array(
|
||||
'host' => isset($hostData[0]) ? $hostData[0] : '',
|
||||
'port' => isset($hostData[1]) ? $hostData[1] : '',
|
||||
$dbConfig = [
|
||||
'host' => $hostData[0] ?? '',
|
||||
'port' => $hostData[1] ?? '',
|
||||
'dbname' => $installData['db-name'],
|
||||
'user' => $installData['db-user-name'],
|
||||
'password' => $installData['db-user-password'],
|
||||
);
|
||||
];
|
||||
|
||||
$mysqlRequirementList = $installer->getSystemRequirementList('database', false, ['database' => $dbConfig]);
|
||||
$mysqlRequirementList = $installer->getSystemRequirementList('database', false, ['databaseParams' => $dbConfig]);
|
||||
$smarty->assign('mysqlRequirementList', $mysqlRequirementList);
|
||||
|
||||
$permissionRequirementList = $installer->getSystemRequirementList('permission');
|
||||
|
||||
@@ -202,8 +202,8 @@ class VarcharFieldTest extends Base
|
||||
$dbHelper = $this->getInjectableFactory()->create(DatabaseHelper::class);
|
||||
|
||||
if (
|
||||
$dbHelper->getDatabaseType() == 'MariaDB'
|
||||
&& version_compare($dbHelper->getDatabaseVersion(), '10.2.7', '>=')
|
||||
$dbHelper->getType() == 'MariaDB'
|
||||
&& version_compare($dbHelper->getVersion(), '10.2.7', '>=')
|
||||
) {
|
||||
$this->assertEquals("'test-default'", $column['COLUMN_DEFAULT']);
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace tests\unit\Espo\Core\ORM;
|
||||
|
||||
use Espo\Core\ORM\DatabaseParamsFactory;
|
||||
use Espo\Core\Utils\Config;
|
||||
|
||||
class DatabaseParamsFactoryTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testCreate(): void
|
||||
{
|
||||
$factory = new DatabaseParamsFactory($this->createConfig());
|
||||
|
||||
$params = $factory->create();
|
||||
|
||||
$this->assertEquals('test:host', $params->getHost());
|
||||
$this->assertEquals(10, $params->getPort());
|
||||
$this->assertEquals('name-db', $params->getName());
|
||||
$this->assertEquals('test-user', $params->getUsername());
|
||||
$this->assertEquals('test-password', $params->getPassword());
|
||||
$this->assertEquals('test-platform', $params->getPlatform());
|
||||
}
|
||||
|
||||
public function testCreateWithMergedAssoc(): void
|
||||
{
|
||||
$factory = new DatabaseParamsFactory($this->createConfig());
|
||||
|
||||
$params = $factory->createWithMergedAssoc([
|
||||
'host' => 'test:host2',
|
||||
'port' => 11,
|
||||
'dbname' => 'name2-db',
|
||||
'user' => 'test-user2',
|
||||
'password' => 'test-password2',
|
||||
]);
|
||||
|
||||
$this->assertEquals('test:host2', $params->getHost());
|
||||
$this->assertEquals(11, $params->getPort());
|
||||
$this->assertEquals('name2-db', $params->getName());
|
||||
$this->assertEquals('test-user2', $params->getUsername());
|
||||
$this->assertEquals('test-password2', $params->getPassword());
|
||||
$this->assertEquals('test-platform', $params->getPlatform());
|
||||
}
|
||||
|
||||
private function createConfig(): Config
|
||||
{
|
||||
$config = $this->createMock(Config::class);
|
||||
|
||||
$config->expects($this->any())
|
||||
->method('get')
|
||||
->willReturnMap([
|
||||
['database', null, ['d' => 'd']],
|
||||
['database.host', null, 'test:host'],
|
||||
['database.port', null, 10],
|
||||
['database.dbname', null, 'name-db'],
|
||||
['database.user', null, 'test-user'],
|
||||
['database.password', null, 'test-password'],
|
||||
['database.platform', null, 'test-platform'],
|
||||
]);
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ class BeforeUpgrade
|
||||
{
|
||||
$databaseHelper = new DatabaseHelper($config);
|
||||
|
||||
$pdo = $databaseHelper->createPdoConnection();
|
||||
$pdo = $databaseHelper->getDatabaseType();
|
||||
|
||||
$query = "
|
||||
ALTER TABLE `user` ADD `working_time_calendar_id` VARCHAR(24)
|
||||
|
||||
Reference in New Issue
Block a user