{"record":{"id":"3331572a313d674b","repo":"phacility/phabricator","slug":"graph-cycle-detected-type-s-cycle-s","errorCode":null,"errorMessage":"Graph cycle detected (type=%s, cycle=%s).","messagePattern":"Graph cycle detected \\(type=(.+?), cycle=(.+?)\\)\\.","errorType":"validation","errorClass":"PhabricatorEdgeCycleException","httpStatus":null,"severity":"error","filePath":"src/infrastructure/edges/editor/PhabricatorEdgeEditor.php","lineNumber":400,"sourceCode":"   */\n  private function detectCycles(array $phids, $edge_type) {\n    // For simplicity, we just seed the graph with the affected nodes rather\n    // than seeding it with their edges. To do this, we just add synthetic\n    // edges from an imaginary '<seed>' node to the known edges.\n\n\n    $graph = id(new PhabricatorEdgeGraph())\n      ->setEdgeType($edge_type)\n      ->addNodes(\n        array(\n          '<seed>' => $phids,\n        ))\n      ->loadGraph();\n\n    foreach ($phids as $phid) {\n      $cycle = $graph->detectCycles($phid);\n      if ($cycle) {\n        throw new PhabricatorEdgeCycleException($edge_type, $cycle);\n      }\n    }\n  }\n\n\n}\n","sourceCodeStart":382,"sourceCodeEnd":407,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php#L382-L407","documentation":"Phabricator's edge editor links objects together (project membership, task dependencies, subproject hierarchies) via a directed graph stored as edges. Before saving, PhabricatorEdgeEditor builds a PhabricatorEdgeGraph seeded with the affected PHIDs and runs detectCycles() on each; if any node can reach itself through the accumulated edges, PhabricatorEdgeCycleException is thrown and the transaction is rejected. This keeps hierarchies (which assume a DAG) acyclic, otherwise queries like ancestor walks would never terminate.","triggerScenarios":"Calling $editor->addEdge($src, $edge_type, $dst)->save() where the reverse path $dst -> ... -> $src already exists for that edge type; e.g. addEdge(A, 'member.project', B) when B is already an ancestor of A in the project-membership graph, or re-parenting a subproject under one of its own descendants.","commonSituations":"Reorganizing project hierarchies in the UI (making P1 a subproject of P2 while P2 is already under P1); scripts or migrations that bulk-insert membership edges without checking existing paths; two admins reparenting the same projects concurrently so the second save creates the loop.","solutions":["Inspect the existing edges of the same type for the destination PHID (Maniphest/project UI or ./bin/search or a management script using PhabricatorEdgeQuery) and remove or repoint the edge that closes the loop.","Reorder the operation: first remove the old parent/dependency edge with removeEdge() and save, then add the new edge that previously would have closed the cycle.","If you drive edge writes from a script, pre-check reachability with PhabricatorEdgeGraph->loadGraph()->detectCycles($src_phid) after simulating the new edge, and skip or report the conflicting pair.","For migrations importing existing data that legitimately contains cycles, break the cycle at import time (drop the lowest-priority edge) rather than trying to bypass the editor - there is no supported way to save a cyclic edge graph."],"exampleFix":"// before: adding the reverse edge directly closes a loop\n$editor = id(new PhabricatorEdgeEditor())\n  ->addEdge($project_a_phid, PhabricatorProjectProjectHasMemberEdgeType::EDGECONST, $project_b_phid)\n  ->save(); // throws PhabricatorEdgeCycleException if B is already above A\n\n// after: remove the opposing hierarchy edge first, then apply the new relation\n$editor = id(new PhabricatorEdgeEditor())\n  ->removeEdge($project_b_phid, PhabricatorProjectProjectHasMemberEdgeType::EDGECONST, $project_a_phid)\n  ->addEdge($project_a_phid, PhabricatorProjectProjectHasMemberEdgeType::EDGECONST, $project_b_phid)\n  ->save();","handlingStrategy":"try-catch","validationCode":"$graph = id(new PhabricatorEdgeGraph())\n  ->setEdgeType($edge_type)\n  ->addNodes(array('<seed>' => array($dst_phid)))\n  ->loadGraph();\n$cycle = $graph->detectCycles($dst_phid);\nif ($cycle) {\n  // adding src->dst would close a loop; refuse or de-conflict first\n  return new Aphront404Response();\n}","typeGuard":null,"tryCatchPattern":"try {\n  id(new PhabricatorEdgeEditor())\n    ->addEdge($src, $edge_type, $dst)\n    ->save();\n} catch (PhabricatorEdgeCycleException $ex) {\n  // surface a user-actionable error: the target is already an ancestor\n  $errors[] = pht(\n    'Cannot create this relation: %s is already above %s in the hierarchy.',\n    $dst_name, $src_name);\n}","preventionTips":["When building hierarchy editors, always execute removeEdge() for the inverse relation in the same editor before addEdge().","Treat detectCycles() as part of your domain model: simulate the edge, check, then save.","In UI flows, disable or warn on moves that would reparent a project under its own descendant."],"tags":["phabricator","edges","graph","cycle","hierarchy","dag"],"backgroundTag":"graph-cycle-detected","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}