*/ public function descendants(CatalogCategory $category): array { $map = $this->includes->edgeMapFor($category->getEntityType(), $category->getEntityId()); return $this->walk((int) $category->getId(), $map); } /** آیا یکی از این دو، دیگری را در بر می‌گیرد؟ (رابطه متقارن نیست، ولی تعارض هست) */ public function overlaps(CatalogCategory $a, CatalogCategory $b): bool { if ($a->getId() === $b->getId()) { return true; } $map = $this->includes->edgeMapFor($a->getEntityType(), $a->getEntityId()); return in_array((int) $b->getId(), $this->walk((int) $a->getId(), $map), true) || in_array((int) $a->getId(), $this->walk((int) $b->getId(), $map), true); } /** * افزودن یال، پیش از ذخیره. * * @throws AppException ۴۲۲ اگر یال دور بسازد */ public function assertNoCycle(CatalogCategory $parent, CatalogCategory $child): void { // اگر والدِ آینده از قبل زیرمجموعهٔ فرزند باشد، این یال حلقه می‌بندد و // `descendants()` تا سرریز استک می‌رود. if ($parent->getId() === $child->getId() || in_array((int) $parent->getId(), $this->descendants($child), true)) { throw new AppException( ErrorCodes::ERR_VALIDATION_001, sprintf('«%s» از قبل زیرمجموعهٔ «%s» است؛ این دو نمی‌توانند شامل هم باشند', $parent->getName(), $child->getName()), 422, 'child_category_uuid', ); } } /** * @param array> $map * @return list */ private function walk(int $rootId, array $map): array { $seen = []; $queue = $map[$rootId] ?? []; while ($queue !== []) { $id = array_shift($queue); // `$seen` هم نتیجه است هم محافظ دور: گرافی که با داده‌های قدیمی حلقه دارد // نباید حلقهٔ بی‌پایان بسازد، حتی اگر ساختش امروز ممنوع است. if (isset($seen[$id])) { continue; } $seen[$id] = true; foreach ($map[$id] ?? [] as $next) { $queue[] = $next; } } return array_map('intval', array_keys($seen)); } }