596 lines
20 KiB
PHP
Executable File
596 lines
20 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* Copyright © Magento, Inc. All rights reserved.
|
|
* See COPYING.txt for license details.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Magento\Test\Integrity\Dependency;
|
|
|
|
use Magento\Framework\App\Utility\Files;
|
|
use Magento\Framework\Component\ComponentRegistrar;
|
|
use Magento\Framework\Setup\Declaration\Schema\Config\Converter;
|
|
use Magento\Framework\Exception\LocalizedException;
|
|
use Magento\TestFramework\Inspection\Exception as InspectionException;
|
|
|
|
/**
|
|
* Provide information on the dependency between the modules according to the declarative schema.
|
|
*
|
|
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
|
|
*/
|
|
class DeclarativeSchemaDependencyProvider
|
|
{
|
|
/**
|
|
* Declarative name for table entity of the declarative schema.
|
|
*/
|
|
public const SCHEMA_ENTITY_TABLE = 'table';
|
|
|
|
/**
|
|
* Declarative name for column entity of the declarative schema.
|
|
*/
|
|
public const SCHEMA_ENTITY_COLUMN = 'column';
|
|
|
|
/**
|
|
* Declarative name for constraint entity of the declarative schema.
|
|
*/
|
|
public const SCHEMA_ENTITY_CONSTRAINT = 'constraint';
|
|
|
|
/**
|
|
* Declarative name for index entity of the declarative schema.
|
|
*/
|
|
public const SCHEMA_ENTITY_INDEX = 'index';
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
private $dbSchemaDeclaration = [];
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
private $moduleSchemaFileMapping = [];
|
|
|
|
/**
|
|
* @var DependencyProvider
|
|
*/
|
|
private $dependencyProvider;
|
|
|
|
/**
|
|
* @param DependencyProvider $dependencyProvider
|
|
*/
|
|
public function __construct(DependencyProvider $dependencyProvider)
|
|
{
|
|
$this->dependencyProvider = $dependencyProvider;
|
|
}
|
|
|
|
/**
|
|
* Provide declared dependencies between modules based on the declarative schema configuration.
|
|
*
|
|
* @param string $moduleName
|
|
* @return array
|
|
* @throws \Exception
|
|
*/
|
|
public function getDeclaredExistingModuleDependencies(string $moduleName): array
|
|
{
|
|
$dependencies = $this->getDependenciesFromFiles($this->getSchemaFileNameByModuleName($moduleName));
|
|
$dependencies = $this->filterSelfDependency($moduleName, $dependencies);
|
|
$declared = $this->dependencyProvider->getDeclaredDependencies(
|
|
$moduleName,
|
|
DependencyProvider::TYPE_HARD,
|
|
DependencyProvider::MAP_TYPE_DECLARED
|
|
);
|
|
|
|
$existingDeclared = [];
|
|
foreach ($dependencies as $dependency) {
|
|
$checkResult = array_intersect($declared, $dependency);
|
|
if ($checkResult) {
|
|
$existingDeclared[] = array_values($checkResult);
|
|
}
|
|
}
|
|
|
|
return array_unique(array_merge([], ...$existingDeclared));
|
|
}
|
|
|
|
/**
|
|
* Provide undeclared dependencies between modules based on the declarative schema configuration.
|
|
*
|
|
* [
|
|
* $dependencyId => [$module1, $module2, $module3 ...],
|
|
* ...
|
|
* ]
|
|
*
|
|
* @param string $moduleName
|
|
* @return array
|
|
* @throws \Exception
|
|
*/
|
|
public function getUndeclaredModuleDependencies(string $moduleName): array
|
|
{
|
|
$dependencies = $this->getDependenciesFromFiles($this->getSchemaFileNameByModuleName($moduleName));
|
|
$dependencies = $this->filterSelfDependency($moduleName, $dependencies);
|
|
return $this->collectDependencies($moduleName, $dependencies);
|
|
}
|
|
|
|
/**
|
|
* Provide schema file name by module name.
|
|
*
|
|
* @param string $module
|
|
* @return string
|
|
* @throws LocalizedException
|
|
*/
|
|
private function getSchemaFileNameByModuleName(string $module): string
|
|
{
|
|
if (empty($this->moduleSchemaFileMapping)) {
|
|
$componentRegistrar = new ComponentRegistrar();
|
|
foreach (array_values(Files::init()->getDbSchemaFiles()) as $filePath) {
|
|
$filePath = reset($filePath);
|
|
foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $moduleDir) {
|
|
if (strpos($filePath, $moduleDir . '/') !== false) {
|
|
$foundModuleName = str_replace('_', '\\', $moduleName);
|
|
$this->moduleSchemaFileMapping[$foundModuleName] = $filePath;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->moduleSchemaFileMapping[$module] ?? '';
|
|
}
|
|
|
|
/**
|
|
* Remove self dependencies.
|
|
*
|
|
* @param string $moduleName
|
|
* @param array $dependencies
|
|
* @return array
|
|
*/
|
|
private function filterSelfDependency(string $moduleName, array $dependencies): array
|
|
{
|
|
foreach ($dependencies as $id => $modules) {
|
|
$decodedId = self::decodeDependencyId($id);
|
|
$entityType = $decodedId['entityType'];
|
|
if ($entityType === self::SCHEMA_ENTITY_TABLE || $entityType === "column") {
|
|
if (array_search($moduleName, $modules) !== false) {
|
|
unset($dependencies[$id]);
|
|
}
|
|
} else {
|
|
$dependencies[$id] = $this->filterComplexDependency($moduleName, $modules);
|
|
}
|
|
}
|
|
|
|
return array_filter($dependencies);
|
|
}
|
|
|
|
/**
|
|
* Remove already declared dependencies.
|
|
*
|
|
* @param string $moduleName
|
|
* @param array $modules
|
|
* @return array
|
|
*/
|
|
private function filterComplexDependency(string $moduleName, array $modules): array
|
|
{
|
|
$resultDependencies = [];
|
|
if (!is_array(reset($modules))) {
|
|
if (array_search($moduleName, $modules) === false) {
|
|
$resultDependencies = $modules;
|
|
}
|
|
} else {
|
|
foreach ($modules as $dependencySet) {
|
|
if (array_search($moduleName, $dependencySet) === false) {
|
|
$resultDependencies[] = $dependencySet;
|
|
}
|
|
}
|
|
$resultDependencies = array_merge([], ...$resultDependencies);
|
|
}
|
|
|
|
return array_values(array_unique($resultDependencies));
|
|
}
|
|
|
|
/**
|
|
* Retrieve declarative schema declaration.
|
|
*
|
|
* @return array
|
|
* @throws LocalizedException
|
|
*/
|
|
private function getDeclarativeSchema(): array
|
|
{
|
|
if ($this->dbSchemaDeclaration) {
|
|
return $this->dbSchemaDeclaration;
|
|
}
|
|
|
|
$entityTypes = [self::SCHEMA_ENTITY_COLUMN, self::SCHEMA_ENTITY_CONSTRAINT, self::SCHEMA_ENTITY_INDEX];
|
|
$declaration = [];
|
|
foreach (Files::init()->getDbSchemaFiles() as $filePath) {
|
|
$filePath = reset($filePath);
|
|
preg_match('#app/code/(\w+/\w+)#', $filePath, $result);
|
|
$moduleName = str_replace('/', '\\', $result[1]);
|
|
$moduleDeclaration = $this->getDbSchemaDeclaration($filePath);
|
|
|
|
foreach ($moduleDeclaration[self::SCHEMA_ENTITY_TABLE] as $tableName => $tableDeclaration) {
|
|
if (!isset($tableDeclaration['modules'])) {
|
|
$tableDeclaration['modules'] = [];
|
|
}
|
|
array_push($tableDeclaration['modules'], $moduleName);
|
|
$moduleDeclaration = array_replace_recursive(
|
|
$moduleDeclaration,
|
|
[self::SCHEMA_ENTITY_TABLE => [
|
|
$tableName => $tableDeclaration,
|
|
]
|
|
]
|
|
);
|
|
foreach ($entityTypes as $entityType) {
|
|
if (!isset($tableDeclaration[$entityType])) {
|
|
continue;
|
|
}
|
|
$moduleDeclaration = array_replace_recursive(
|
|
$moduleDeclaration,
|
|
[self::SCHEMA_ENTITY_TABLE => [
|
|
$tableName => $this->addModuleAssigment($tableDeclaration, $entityType, $moduleName)
|
|
]
|
|
]
|
|
);
|
|
}
|
|
}
|
|
$declaration = array_merge_recursive($declaration, $moduleDeclaration);
|
|
}
|
|
$this->dbSchemaDeclaration = $declaration;
|
|
|
|
return $this->dbSchemaDeclaration;
|
|
}
|
|
|
|
/**
|
|
* Get declared dependencies.
|
|
*
|
|
* @param string $tableName
|
|
* @param string $entityType
|
|
* @param null|string $entityName
|
|
* @return array
|
|
* @throws LocalizedException
|
|
*/
|
|
private function resolveEntityDependencies(string $tableName, string $entityType, ?string $entityName = null): array
|
|
{
|
|
switch ($entityType) {
|
|
case self::SCHEMA_ENTITY_COLUMN:
|
|
case self::SCHEMA_ENTITY_CONSTRAINT:
|
|
case self::SCHEMA_ENTITY_INDEX:
|
|
return $this->getDeclarativeSchema()
|
|
[self::SCHEMA_ENTITY_TABLE][$tableName][$entityType][$entityName]['modules'];
|
|
case self::SCHEMA_ENTITY_TABLE:
|
|
return $this->getDeclarativeSchema()[self::SCHEMA_ENTITY_TABLE][$tableName]['modules'];
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $filePath
|
|
* @return array
|
|
*/
|
|
private function getDbSchemaDeclaration(string $filePath): array
|
|
{
|
|
$dom = new \DOMDocument();
|
|
$dom->loadXML(file_get_contents($filePath));
|
|
return (new Converter())->convert($dom);
|
|
}
|
|
|
|
/**
|
|
* Add dependency on the current module.
|
|
*
|
|
* @param array $tableDeclaration
|
|
* @param string $entityType
|
|
* @param string $moduleName
|
|
* @return array
|
|
*/
|
|
private function addModuleAssigment(
|
|
array $tableDeclaration,
|
|
string $entityType,
|
|
string $moduleName
|
|
): array {
|
|
$declarationWithAssigment = [];
|
|
foreach ($tableDeclaration[$entityType] as $entityName => $entityDeclaration) {
|
|
if (!isset($entityDeclaration['modules'])) {
|
|
$entityDeclaration['modules'] = [];
|
|
}
|
|
if (!$this->isEntityDisabled($entityDeclaration)) {
|
|
array_push($entityDeclaration['modules'], $moduleName);
|
|
}
|
|
|
|
$declarationWithAssigment[$entityType][$entityName] = $entityDeclaration;
|
|
}
|
|
|
|
return $declarationWithAssigment;
|
|
}
|
|
|
|
/**
|
|
* Retrieve dependencies from files.
|
|
*
|
|
* @param string $file
|
|
* @return string[]
|
|
* @throws \Exception
|
|
*/
|
|
private function getDependenciesFromFiles($file)
|
|
{
|
|
if (!$file) {
|
|
return [];
|
|
}
|
|
|
|
$moduleDbSchema = $this->getDbSchemaDeclaration($file);
|
|
$dependencies = array_merge_recursive(
|
|
$this->getDisabledDependencies($moduleDbSchema),
|
|
$this->getConstraintDependencies($moduleDbSchema),
|
|
$this->getIndexDependencies($moduleDbSchema)
|
|
);
|
|
return $dependencies;
|
|
}
|
|
|
|
/**
|
|
* Retrieve dependencies for disabled entities.
|
|
*
|
|
* @param array $moduleDeclaration
|
|
* @return array
|
|
* @throws LocalizedException
|
|
*/
|
|
private function getDisabledDependencies(array $moduleDeclaration): array
|
|
{
|
|
$disabledDependencies = [];
|
|
$entityTypes = [self::SCHEMA_ENTITY_COLUMN, self::SCHEMA_ENTITY_CONSTRAINT, self::SCHEMA_ENTITY_INDEX];
|
|
foreach ($moduleDeclaration[self::SCHEMA_ENTITY_TABLE] as $tableName => $tableDeclaration) {
|
|
foreach ($entityTypes as $entityType) {
|
|
if (!isset($tableDeclaration[$entityType])) {
|
|
continue;
|
|
}
|
|
foreach ($tableDeclaration[$entityType] as $entityName => $entityDeclaration) {
|
|
if ($this->isEntityDisabled($entityDeclaration)) {
|
|
$dependencyIdentifier = $this->getDependencyId($tableName, $entityType, $entityName);
|
|
$disabledDependencies[$dependencyIdentifier] =
|
|
$this->resolveEntityDependencies($tableName, $entityType, $entityName);
|
|
}
|
|
}
|
|
}
|
|
if ($this->isEntityDisabled($tableDeclaration)) {
|
|
$disabledDependencies[$this->getDependencyId($tableName)] =
|
|
$this->resolveEntityDependencies($tableName, self::SCHEMA_ENTITY_TABLE);
|
|
}
|
|
}
|
|
|
|
return $disabledDependencies;
|
|
}
|
|
|
|
/**
|
|
* Retrieve dependencies for foreign entities.
|
|
*
|
|
* @param array $constraintDeclaration
|
|
* @return array
|
|
* @throws \Exception
|
|
*/
|
|
private function getFKDependencies(array $constraintDeclaration): array
|
|
{
|
|
$referenceDependencyIdentifier =
|
|
$this->getDependencyId(
|
|
$constraintDeclaration['referenceTable'],
|
|
self::SCHEMA_ENTITY_CONSTRAINT,
|
|
$constraintDeclaration['referenceId']
|
|
);
|
|
$dependencyIdentifier =
|
|
$this->getDependencyId(
|
|
$constraintDeclaration[self::SCHEMA_ENTITY_TABLE],
|
|
self::SCHEMA_ENTITY_CONSTRAINT,
|
|
$constraintDeclaration['referenceId']
|
|
);
|
|
|
|
$constraintDependencies = [];
|
|
$constraintDependencies[$referenceDependencyIdentifier] =
|
|
$this->resolveEntityDependencies(
|
|
$constraintDeclaration['referenceTable'],
|
|
self::SCHEMA_ENTITY_COLUMN,
|
|
$constraintDeclaration['referenceColumn']
|
|
);
|
|
$constraintDependencies[$dependencyIdentifier] =
|
|
$this->resolveEntityDependencies(
|
|
$constraintDeclaration[self::SCHEMA_ENTITY_TABLE],
|
|
self::SCHEMA_ENTITY_COLUMN,
|
|
$constraintDeclaration[self::SCHEMA_ENTITY_COLUMN]
|
|
);
|
|
|
|
return $constraintDependencies;
|
|
}
|
|
|
|
/**
|
|
* Retrieve dependencies for constraint entities.
|
|
*
|
|
* @param array $moduleDeclaration
|
|
* @return array
|
|
* @throws \Exception
|
|
*/
|
|
private function getConstraintDependencies(array $moduleDeclaration): array
|
|
{
|
|
$constraintDependencies = [];
|
|
foreach ($moduleDeclaration[self::SCHEMA_ENTITY_TABLE] as $tableName => $tableDeclaration) {
|
|
if (empty($tableDeclaration[self::SCHEMA_ENTITY_CONSTRAINT])) {
|
|
continue;
|
|
}
|
|
foreach ($tableDeclaration[self::SCHEMA_ENTITY_CONSTRAINT] as $constraintName => $constraintDeclaration) {
|
|
if ($this->isEntityDisabled($constraintDeclaration)) {
|
|
continue;
|
|
}
|
|
$dependencyIdentifier =
|
|
$this->getDependencyId($tableName, self::SCHEMA_ENTITY_CONSTRAINT, $constraintName);
|
|
switch ($constraintDeclaration['type']) {
|
|
case 'foreign':
|
|
//phpcs:ignore Magento2.Performance.ForeachArrayMerge
|
|
$constraintDependencies = array_merge(
|
|
$constraintDependencies,
|
|
$this->getFKDependencies($constraintDeclaration)
|
|
);
|
|
break;
|
|
case 'primary':
|
|
case 'unique':
|
|
$constraintDependencies[$dependencyIdentifier] = $this->getComplexDependency(
|
|
$tableName,
|
|
$constraintDeclaration
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return $constraintDependencies;
|
|
}
|
|
|
|
/**
|
|
* Calculate complex dependency.
|
|
*
|
|
* @param string $tableName
|
|
* @param array $entityDeclaration
|
|
* @return array
|
|
* @throws LocalizedException
|
|
*/
|
|
private function getComplexDependency(string $tableName, array $entityDeclaration): array
|
|
{
|
|
$complexDependency = [];
|
|
if (empty($entityDeclaration[self::SCHEMA_ENTITY_COLUMN])) {
|
|
return $complexDependency;
|
|
}
|
|
|
|
if (!is_array($entityDeclaration[self::SCHEMA_ENTITY_COLUMN])) {
|
|
$entityDeclaration[self::SCHEMA_ENTITY_COLUMN] = [$entityDeclaration[self::SCHEMA_ENTITY_COLUMN]];
|
|
}
|
|
|
|
foreach (array_keys($entityDeclaration[self::SCHEMA_ENTITY_COLUMN]) as $columnName) {
|
|
$complexDependency[] =
|
|
$this->resolveEntityDependencies($tableName, self::SCHEMA_ENTITY_COLUMN, $columnName);
|
|
}
|
|
|
|
return array_values($complexDependency);
|
|
}
|
|
|
|
/**
|
|
* Retrieve dependencies for index entities.
|
|
*
|
|
* @param array $moduleDeclaration
|
|
* @return array
|
|
* @throws LocalizedException
|
|
*/
|
|
private function getIndexDependencies(array $moduleDeclaration): array
|
|
{
|
|
$indexDependencies = [];
|
|
foreach ($moduleDeclaration[self::SCHEMA_ENTITY_TABLE] as $tableName => $tableDeclaration) {
|
|
if (empty($tableDeclaration[self::SCHEMA_ENTITY_INDEX])) {
|
|
continue;
|
|
}
|
|
foreach ($tableDeclaration[self::SCHEMA_ENTITY_INDEX] as $indexName => $indexDeclaration) {
|
|
if ($this->isEntityDisabled($indexDeclaration)) {
|
|
continue;
|
|
}
|
|
$dependencyIdentifier =
|
|
$this->getDependencyId($tableName, self::SCHEMA_ENTITY_INDEX, $indexName);
|
|
$indexDependencies[$dependencyIdentifier] =
|
|
$this->getComplexDependency($tableName, $indexDeclaration);
|
|
}
|
|
}
|
|
|
|
return $indexDependencies;
|
|
}
|
|
|
|
/**
|
|
* Check status of the entity declaration.
|
|
*
|
|
* @param array $entityDeclaration
|
|
* @return bool
|
|
*/
|
|
private function isEntityDisabled(array $entityDeclaration): bool
|
|
{
|
|
return isset($entityDeclaration['disabled']) && $entityDeclaration['disabled'] == true;
|
|
}
|
|
|
|
/**
|
|
* Retrieve dependency id.
|
|
*
|
|
* @param string $tableName
|
|
* @param string $entityType
|
|
* @param null|string $entityName
|
|
* @return string
|
|
*/
|
|
private function getDependencyId(
|
|
string $tableName,
|
|
string $entityType = self::SCHEMA_ENTITY_TABLE,
|
|
?string $entityName = null
|
|
) {
|
|
return implode('___', [$tableName, $entityType, $entityName ?: $tableName]);
|
|
}
|
|
|
|
/**
|
|
* Retrieve dependency parameters from dependency id.
|
|
*
|
|
* @param string $id
|
|
* @return array
|
|
*/
|
|
public static function decodeDependencyId(string $id): array
|
|
{
|
|
$decodedValues = explode('___', $id);
|
|
$result = [
|
|
'tableName' => $decodedValues[0],
|
|
'entityType' => $decodedValues[1],
|
|
'entityName' => $decodedValues[2],
|
|
];
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Collect module dependencies.
|
|
*
|
|
* @param $currentModuleName
|
|
* @param array $dependencies
|
|
* @return array
|
|
* @throws InspectionException
|
|
* @throws LocalizedException
|
|
*/
|
|
private function collectDependencies($currentModuleName, $dependencies = []): array
|
|
{
|
|
if (empty($dependencies)) {
|
|
return [];
|
|
}
|
|
foreach ($dependencies as $dependencyName => $dependency) {
|
|
$this->collectDependency($dependencyName, $dependency, $currentModuleName);
|
|
}
|
|
|
|
return $this->dependencyProvider->getDeclaredDependencies(
|
|
$currentModuleName,
|
|
DependencyProvider::TYPE_HARD,
|
|
DependencyProvider::MAP_TYPE_FOUND
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Collect a module dependency.
|
|
*
|
|
* @param string $dependencyName
|
|
* @param array $dependency
|
|
* @param string $currentModule
|
|
* @throws LocalizedException
|
|
* @throws InspectionException
|
|
*/
|
|
private function collectDependency(
|
|
string $dependencyName,
|
|
array $dependency,
|
|
string $currentModule
|
|
) {
|
|
$declared = $this->dependencyProvider->getDeclaredDependencies(
|
|
$currentModule,
|
|
DependencyProvider::TYPE_HARD,
|
|
DependencyProvider::MAP_TYPE_DECLARED
|
|
);
|
|
$checkResult = array_intersect($declared, $dependency);
|
|
|
|
if (empty($checkResult)) {
|
|
$this->dependencyProvider->addDependencies(
|
|
$currentModule,
|
|
DependencyProvider::TYPE_HARD,
|
|
DependencyProvider::MAP_TYPE_FOUND,
|
|
[
|
|
$dependencyName => $dependency,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
}
|