Advent of code 2018/20
Ajax Direct

Answer 655ms

[VISUALISATION] Part 1 : 3930 Part 2 : 8240
const TO   = ["N" => 1, "E" => 2, "S" => 4, "W" => 8];
const FROM = ["S" => 1, "W" => 2, "N" => 4, "E" => 8];

function get_grid($input) {
    $grid  = [];
    $queue = [[[0, 0], substr($input, 1, -1)]];

    while (count($queue)) {
        [$pos, $path] = array_shift($queue);
        if (empty($path)) continue;

        // Get direct path until firt intersection is met
        $direct = strpos($path, "(") !== false ? substr($path, 0, strpos($path, "(")) : $path;

        // Follow path, save open doors each way using their bit values
        for ($c = 0; $c < strlen($direct); $c++) {
            $grid[$pos[1]][$pos[0]] = ($grid[$pos[1]][$pos[0]] ?? 0) | TO[$direct[$c]];
            match ($direct[$c]) {
                "N" => $pos[1]--,
                "E" => $pos[0]++,
                "S" => $pos[1]++,
                "W" => $pos[0]--,
            };
            $grid[$pos[1]][$pos[0]] = ($grid[$pos[1]][$pos[0]] ?? 0) | FROM[$direct[$c]];
        }

        // Split intersection and queue each sub-path from the current position...
        $rest = substr($path, strlen($direct) + 1);
        if (!$rest) continue;

        // ... find intersection group
        $o = 1; $c = 0; $i = 0;
        while ($o > $c) match ($rest[$i++]) {
            "(" => $o++,
            ")" => $c++,
            default => null
        };
        $group = substr($rest, 0, $i - 1);

        // ... and queue each first-level parts
        $item = ""; $o = 0; $c = 0;
        for ($i = 0; $i < strlen($group); $i++) {
            if ($group[$i] == "|" && $o == $c) {
                $queue[] = [$pos, $item];
                $item = "";
                continue;
            }
            if ($group[$i] == "(") $o++;
            if ($group[$i] == ")") $c++;
            $item .= $group[$i];
        }
        $queue[] = [$pos, $item];

        // Queue the rest
        $rest = substr($rest, $i + 1);
        $queue[] = [$pos, $rest];
    }
    return $grid;
}

// ==================================================
// > PART 1
// ==================================================
$grid = get_grid($input);

// Explore all doors
$graph = new Graph(function ($graph) use ($grid) {
    [$x, $y] = explode(";", $graph->current);
    $doors = array_filter(array_keys(TO), fn ($door) => $grid[$y][$x] & TO[$door]);

    $rooms = (array) set([
        "N" => [$x, $y - 1],
        "E" => [$x + 1, $y],
        "S" => [$x, $y + 1],
        "W" => [$x - 1, $y],
    ])->keepKeys($doors)->map(fn ($xy) => implode(";", $xy));

    return array_fill_keys($rooms, 1);
});

// When everything was explored, return furthest room
$graph->onCompletion(function ($graph) {
    return max($graph->values);
});

$solution_1 = $graph->explore("0;0");

// ==================================================
// > PART 2
// ==================================================
$solution_2 = set($graph->values)->filter(fn ($v) => $v >= 1000)->count();