grand refactoring of schema and other database related things

This commit is contained in:
Yuri Kuznetsov
2023-01-28 12:45:35 +02:00
parent 7d4ded2480
commit 03c123d63e
49 changed files with 2445 additions and 1867 deletions
+25 -61
View File
@@ -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);
}
}
@@ -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}.");
@@ -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;
}
}
+60 -223
View File
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
@@ -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;
}
@@ -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);
}
}
}
@@ -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') ?? []);
}
}
+8 -3
View File
@@ -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']);
+2 -1
View File
@@ -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']) {
+5 -5
View File
@@ -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;
}
}
+1 -1
View File
@@ -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)