PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

BFS and DFS – graph traversal, shortest path, cycle detection

by Henryk Tews / Tuesday, 11 May 2021 / Published in Algorytmy

BFS and DFS are fundamental graph traversal algorithms. They appear in unexpected places in web development: finding the shortest path between categories in a tree, detecting circular dependencies between modules, or checking if two nodes in a social graph are connected. I implement both in PHP with practical examples.

Graph representation in PHP

<?php

// Adjacency list - efficient for sparse graphs (most real-world cases)
$graph = [
    'A' => ['B', 'C'],
    'B' => ['A', 'D', 'E'],
    'C' => ['A', 'F'],
    'D' => ['B'],
    'E' => ['B', 'F'],
    'F' => ['C', 'E'],
];
// A connects to B and C, B connects to A, D and E, etc.

BFS – Breadth-First Search

BFS explores the graph level by level using a queue. Guarantees finding the shortest path in an unweighted graph.

<?php

declare(strict_types=1);

function bfs(array $graph, string $start): array
{
    $visited = [];
    $queue   = new \SplQueue();
    $order   = [];

    $queue->enqueue($start);
    $visited[$start] = true;

    while (!$queue->isEmpty()) {
        $node    = $queue->dequeue();
        $order[] = $node;

        foreach ($graph[$node] ?? [] as $neighbour) {
            if (!isset($visited[$neighbour])) {
                $visited[$neighbour] = true;
                $queue->enqueue($neighbour);
            }
        }
    }

    return $order;
}

$result = bfs($graph, 'A');
print_r($result); // ['A', 'B', 'C', 'D', 'E', 'F'] - level by level

// Shortest path with BFS
function shortestPath(array $graph, string $start, string $end): ?array
{
    if ($start === $end) return [$start];

    $queue    = new \SplQueue();
    $visited  = [$start => true];
    $parents  = [$start => null];

    $queue->enqueue($start);

    while (!$queue->isEmpty()) {
        $node = $queue->dequeue();

        foreach ($graph[$node] ?? [] as $neighbour) {
            if (isset($visited[$neighbour])) continue;

            $visited[$neighbour]  = true;
            $parents[$neighbour]  = $node;

            if ($neighbour === $end) {
                // Reconstruct path
                $path = [];
                $current = $end;
                while ($current !== null) {
                    array_unshift($path, $current);
                    $current = $parents[$current];
                }
                return $path;
            }

            $queue->enqueue($neighbour);
        }
    }

    return null; // no path found
}

$path = shortestPath($graph, 'A', 'F');
print_r($path); // ['A', 'C', 'F'] - 2 steps, shortest route

DFS – Depth-First Search

DFS explores as deep as possible before backtracking. Naturally implemented recursively, or iteratively with a stack.

<?php

declare(strict_types=1);

// Recursive DFS
function dfsRecursive(array $graph, string $node, array &$visited = []): array
{
    $visited[$node] = true;
    $order          = [$node];

    foreach ($graph[$node] ?? [] as $neighbour) {
        if (!isset($visited[$neighbour])) {
            $order = array_merge($order, dfsRecursive($graph, $neighbour, $visited));
        }
    }

    return $order;
}

// Iterative DFS (avoids deep recursion)
function dfsIterative(array $graph, string $start): array
{
    $visited = [];
    $stack   = new \SplStack();
    $order   = [];

    $stack->push($start);

    while (!$stack->isEmpty()) {
        $node = $stack->pop();

        if (isset($visited[$node])) continue;
        $visited[$node] = true;
        $order[]        = $node;

        // Push neighbours in reverse order to maintain left-to-right traversal
        foreach (array_reverse($graph[$node] ?? []) as $neighbour) {
            if (!isset($visited[$neighbour])) {
                $stack->push($neighbour);
            }
        }
    }

    return $order;
}

$resultDfs = dfsIterative($graph, 'A');
print_r($resultDfs); // ['A', 'B', 'D', 'E', 'F', 'C'] - depth first

Cycle detection with DFS

<?php

// Detect circular dependencies between Magento modules
function hasCycle(array $dependencies): bool
{
    $visited    = [];
    $inProgress = [];

    function dfsHasCycle(string $node, array $deps, array &$visited, array &$inProgress): bool
    {
        $visited[$node]    = true;
        $inProgress[$node] = true;

        foreach ($deps[$node] ?? [] as $dep) {
            if (!isset($visited[$dep])) {
                if (dfsHasCycle($dep, $deps, $visited, $inProgress)) {
                    return true;
                }
            } elseif (isset($inProgress[$dep])) {
                return true; // back edge - cycle found
            }
        }

        unset($inProgress[$node]);
        return false;
    }

    foreach (array_keys($dependencies) as $node) {
        if (!isset($visited[$node])) {
            if (dfsHasCycle($node, $dependencies, $visited, $inProgress)) {
                return true;
            }
        }
    }

    return false;
}

// Example: module dependencies
$deps = [
    'ModuleA' => ['ModuleB', 'ModuleC'],
    'ModuleB' => ['ModuleD'],
    'ModuleC' => ['ModuleD'],
    'ModuleD' => [],
    // 'ModuleD' => ['ModuleA'],  // this would create a cycle
];

echo hasCycle($deps) ? 'Circular dependency detected!' : 'No cycles - OK';

BFS vs DFS – when to use which

Criterion BFS DFS
Shortest path Yes (unweighted) No
Memory usage O(w) – width of graph O(d) – depth of graph
Cycle detection Possible Natural
Topological sort No Yes
Finding all paths Possible Natural
Very deep graphs Better Stack overflow risk with recursion

Summary

BFS and DFS are not just academic exercises – they appear in real projects whenever you work with hierarchical data (category trees), dependency graphs (module loading order), or network structures (related products). BFS guarantees the shortest path in unweighted graphs; DFS is natural for cycle detection and topological sorting. Both run in O(V + E) time – vertices plus edges.

About Henryk Tews

What you can read next

Trie – prefix tree, autocomplete, spam filter, benchmark vs array
LRU Cache – O(1) implementation with HashMap and doubly linked list, per-request cache
Sorting in PHP – bubble sort, merge sort, usort() and the spaceship operator <=>

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}