Advent of code 2019/20
Ajax Direct

Answer

Part 1 :
Part 2 :
$grid = grid($input);
[$w, $h] = [$grid->width(), $grid->height()];

// Parse all portals
[$portals, $outer, $inner] = set($grid->searchAll("."))->reduce(function ($carry, $xy) use ($grid, $w, $h) {
    $n = $grid->getNeighbors($xy)->keep(range("A", "Z"));
    if ($n->empty()) return $carry;
    $first = $n->values()[0];

    $nn = $grid->getNeighbors(explode(";", $n->keys()[0]));
    $second = $nn->keep(range("A", "Z"))->values()[0];

    $dir = $nn->values()->search($second);
    $portal = $dir == 1 || $dir == 2 ? $first . $second : $second . $first;
    $carry[0][$portal][] = $xy;
    $carry[($xy[1] == 2 || $xy[1] == $h - 3 || $xy[0] == 2 || $xy[0] == $w - 3) ? 1 : 2][] = implode(";", $xy);
    return $carry;
}, [[], [], []]);

$shortcuts = set([]);
foreach ($portals as $xy) {
    if (count($xy) > 1) {
        $shortcuts[implode(";", $xy[0])] = implode(";", $xy[1]);
        $shortcuts[implode(";", $xy[1])] = implode(";", $xy[0]);
    }
}

$outer = array_diff($outer, [implode(";", $portals["AA"][0]), implode(";", $portals["ZZ"][0])]);

// ==================================================
// > PART 1 : Could be optimized by creating simpler graph of portal to portal
// ==================================================
[$path, $solution_1] = (new Graph(function ($graph) use ($grid, $shortcuts) {
    $pos = explode(";", $graph->current);
    return $grid->getNeighbors($pos)->keep(".")->map(fn () => 1)
    ->mapAssoc(fn ($k, $v) => isset($shortcuts[$k]) ? [$shortcuts[$k] => 2] : [$k => 1]);
}))
->explore(implode(";", $portals["AA"][0]), implode(";", $portals["ZZ"][0]));

// ==================================================
// > PART 2 : same
// ==================================================
[$path, $solution_2] = (new Graph(function ($graph) use ($grid, $shortcuts, $inner, $outer) {
    [$pos, $depth] = explode("|", $graph->current);
    $pos = explode(";", $pos); $depth = (int) $depth;

    return $grid->getNeighbors($pos)->keep(".")->map(fn () => 1)
        ->mapAssoc(function ($k, $v) use ($depth, $inner, $outer, $shortcuts) {
            [$state, $distance] = isset($shortcuts[$k]) ? [$shortcuts[$k], 2] : [$k, 1];
            $new_depth = ($depth + (in_array($k, $inner) ? 1 : (in_array($k, $outer) ? -1 : 0)));
            if ($new_depth < 0) return [];
            return [$state . "|" . $new_depth => $distance];
        });
}))
->explore(implode(";", $portals["AA"][0])."|0", implode(";", $portals["ZZ"][0])."|0");