После нескольких проб и ошибок вот что я придумал.
Учитывая плоский список отношений, он строит поиск, который затем используется в рекурсивной функции обхода.Функция обхода «запоминает» исходную часть, которая была запрошена ($initialPart
), и выдает исключение при обнаружении цикла.Например, исключение: Loop detected: f=>c=>d=>f!
Сценарий:
$rows = [
['assemblyId' => 'a', 'partId' => 'b'],
['assemblyId' => 'a', 'partId' => 'c'],
['assemblyId' => 'c', 'partId' => 'd'],
['assemblyId' => 'd', 'partId' => 'e'],
['assemblyId' => 'd', 'partId' => 'f'],
['assemblyId' => 'f', 'partId' => 'c'],
['assemblyId' => 'f', 'partId' => 'g'],
['assemblyId' => 'g', 'partId' => 'h'],
['assemblyId' => 'g', 'partId' => 'i'],
['assemblyId' => 'g', 'partId' => 'j'],
];
$assemblyLookup = [];
foreach ($rows as $row) {
$assemblyLookup[$row['assemblyId']][$row['partId']] = $row;
}
function getTree(&$part, $initialPart = null, $path = '', $level = 0, $maxLevel = 100) {
global $assemblyLookup;
if ($initialPart === null) {
$initialPart = $part;
}
if ($level >= $maxLevel) {
return;
}
$path .= '=>' . $part['partId'];
if (is_array($assemblyLookup[$part['partId']])) {
$lookup = $assemblyLookup[$part['partId']];
foreach ($lookup as $child) {
if ($child['partId'] === $initialPart['partId']) {
$circularPath = substr($path, 2) . '=>' . $child['partId'] . '!';
throw new Exception("Loop detected: " . $circularPath);
return;
}
$part['children'][$child['partId']] = $child;
getTree($part['children'][$child['partId']], $initialPart, $path, ++$level, $maxLevel);
}
} else {
$path .= ';';
$part['path'] = substr($path, 2); // Strip arrow from left side
}
return $part;
}
$part = ['partId' => 'f'];
$tree = getTree($part);