Advent of code 2023/12
Ajax Direct

Answer

Part 1 :
Part 2 :
function positions($string, $size) {
    global $pos_cache;
    $key = $string."|".$size;
    if (isset($pos_cache[$key])) return $pos_cache[$key];

    preg_match_all("/(?<=#{{$size}})/", str_replace("?", "#", $string), $pos, PREG_OFFSET_CAPTURE);
    $pos = array_filter(array_column($pos[0], 1), function ($p) use ($string, $size) {
        if (strpos(substr($string, 0, $p - $size), "#") !== false) return false;
        if (($string[$p]??false) == "#") return false;
        return true;
    });

    return $pos_cache[$key] = $pos;
}

function arrangements($string, $numbers) {
    global $cache;
    $key = $string."|".join(",", $numbers);
    if (isset($cache[$key])) return $cache[$key];

    // All numbers placed : 1 arrangement found if no # left
    if (!$numbers) return strpos($string, "#") === false ? 1 : 0;

    // Get the first number to place
    $n = array_shift($numbers);

    // Place it to all possible positions, and recurse with the rest of the number/string
    $sum = 0;
    foreach (positions($string, $n) as $p) {
        $nstring = $string;
        foreach (range($p - $n, $p - 1) as $i) $nstring[$i] = "#";
        $nstring = substr($nstring, $p + 1);
        $sum += arrangements($nstring, $numbers);
    }

    $cache[$key] = $sum;
    return $cache[$key];
}

// ==================================================
// > SOLVE
// ==================================================
[$solution_1, $solution_2] = $input->lines->reduce(function ($solutions, $line) {
    [$string, $counts] = $line->split(" ");

    return [
        // Part 1
        $solutions[0] + arrangements($string, explode(",", $counts)),
        // Part 2
        $solutions[1] + arrangements(
            implode("?", array_fill(0, 5, $string)),
            array_merge(...array_fill(0, 5, explode(",", $counts)))
        )
    ];
}, [0, 0]);