Advent of code 2023/17
Ajax Direct

Answer 8059ms

Part 1 : 785 Part 2 : 922
function solve($input, $ultra = false) {
    $grid = array_map("str_split", explode("\n", $input));
    $goal = [count($grid[0]) - 1, count($grid) - 1];

    return (new Graph(function ($graph) use ($grid, $ultra) {
        [$x, $y, $cons, $dir] = explode(";", $graph->current);
        $neighbors = neighbors([$x, $y]);

        // Can't reverse
        unset($neighbors[["up" => "down", "down" => "up", "left" => "right",  "right" => "left"][$dir]??""]);

        // Can't move 3 blocks in the same direction, or 10 for ultra
        if ($cons >= 10 || (!$ultra && $cons >= 3)) unset($neighbors[$dir]);

        // Ultra : Can't turn bellow 4
        if ($ultra && $cons < 4 && $dir != "none") {
            $neighbors = array_intersect_key($neighbors, [$dir => true]);
        }

        foreach ($neighbors as $n=>[$nx,$ny]) {
            if (!isset($grid[$ny][$nx])) continue; // OOB
            $next["$nx;$ny;".($n==$dir?$cons+1:1).";$n"] = $grid[$ny][$nx];
        }
        return $next ?? [];
    }))
    // End when end is reached
    ->defineEnd(function ($graph) use ($goal, $ultra) {
        [$x, $y, $cons] = explode(";", $graph->current);
        if ($ultra && $cons < 4) return false;
        return [$x, $y] == $goal;
    })
    // A*
    ->definePriority(function ($graph, $state, $value) use ($goal) {
        [$x, $y] = explode(";", $graph->current);
        return $value + manhattan([$x, $y], $goal) * 2;
    })
    ->explore("0;0;0;none")[1];
}

// ==================================================
// > SOLVE
// ==================================================
$solution_1 = solve($input, false);
$solution_2 = solve($input, true);