Advent of code 2022/22
Ajax Direct

Answer

Part 1 :
Part 2 :
function get_solution($self, $grid, $steps, $rotations, $wrap_selection = null)
{
    $facings = set([">", "v", "<", "^"]);
    $facing  = ">";

    while ($steps->count() || $rotations->count()) {

        // Move steps but stop at first rock hit
        $direction = direction($facing);
        $infront = $grid->slice($self, $direction);
        $step    = (int) $steps->shift();
        $edge    = $infront->count();
        $rock    = $infront->search("#");
        $self->direction(
            $direction,
            min($step, $edge, $rock === false ? INF : $rock)
        );

        // Wrap arround if we met the edge before finishing our steps
        if ($rock === false && $step > $edge) {
            $step -= $edge;

            if ($wrap_selection) {
                [$wrap, $new_facing] = $wrap_selection($self, $facing, $grid);
            } else {
                [$wrap, $new_facing] = [$grid->wrap($self->getNeighbor($direction), $direction), $facing];
            }

            // Not a rock, go to the wrap point and continue from there
            if ($grid->get($wrap) != "#") {
                $self->setXY($wrap);
                $steps->unshift($step - 1);
                $facing = $new_facing;
                continue;
            }
        }

        // Apply rotation
        if (!$rotations->count()) continue;
        $facing = $facings[($facings->search($facing) + ["R" => 1, "L" => 3][$rotations->shift()]) % 4];
    }

    return ($self[1] + 1) * 1000 + ($self[0] + 1) * 4 + $facings->search($facing);
}

// Parse the input, make a grid and get steps/rotations
[$map, $moves] = $input->split("\n\n");

// Grid and start position
$grid = grid($map, 1)->filter();
$start = xy([$grid->rows[0]->keys()->first(), 0]);

// Steps and rotations
preg_match_all("/[0-9]+/", $moves, $steps);
preg_match_all("/[A-Z]+/", $moves, $rotations);

// Solution 1 : default wrap method
$solution_1 = get_solution(clone $start, clone $grid, set($steps[0]), set($rotations[0]));

// Solution 2 : custom wrap using a cube
$edges = [
    "1:0" => ["^" => ["0:3", ">", false], "<" => ["0:2", ">", true]],
    "2:0" => ["^" => ["0:3", "^", false], ">" => ["1:2", "<", true], "v" => ["1:1", "<", false]],
    "1:1" => [">" => ["2:0", "^", false], "<" => ["0:2", "v", false]],
    "0:2" => ["^" => ["1:1", ">", false], "<" => ["1:0", ">", true]],
    "1:2" => [">" => ["2:0", "<", true],  "v" => ["0:3", "<", false]],
    "0:3" => [">" => ["1:2", "^", false], "v" => ["2:0", "v", false], "<" => ["1:0", "v", false]],
];

$solution_2 = get_solution(clone $start, clone $grid, set($steps[0]), set($rotations[0]), function ($pos, $facing, $grid) use ($edges) {
    $face    = floor($pos[0] / 50).":".floor($pos[1] / 50);
    [$x, $y] = [$pos[0] % 50, $pos[1] % 50];
    [$new_face, $new_facing, $invert] = $edges[$face][$facing];

    switch ($new_facing) {
        case ">":
            $nx = 0;
            $ny = abs(($invert ? 49 : 0) - (in_array($facing, ["v", "^"]) ? $x : $y));
            break;
        case "<":
            $nx = 49;
            $ny = abs(($invert ? 49 : 0) - (in_array($facing, ["v", "^"]) ? $x : $y));
            break;
        case "^":
            $nx = abs(($invert ? 49 : 0) - (in_array($facing, ["v", "^"]) ? $x : $y));
            $ny = 49;
            break;
        case "v":
            $nx = abs(($invert ? 49 : 0) - (in_array($facing, ["v", "^"]) ? $x : $y));
            $ny = 0;
            break;
    }

    $mult = explode(":", $new_face);
    $wrap = [[$mult[0]*50 + $nx, $mult[1]*50 + $ny], $new_facing];
    return $wrap;
});