Advent of code 2019/24
Ajax Direct

Answer 3178ms

Part 1 : 17863741 Part 2 : 2029
/**
 * Return the mutated grid.
 * Take a callback as second argument, that specifies how many bugs are surounding a given position.
 *
 * @param array $grid
 * @param callable $count_neighbors_bugs
 * @return array
 */
function mutate($grid, $count_neighbors_bugs) {
    $ngrid = [];

    for ($y = 0; $y < 5; $y++) for ($x = 0; $x < 5; $x++) {
        $nbugs = $count_neighbors_bugs([$x, $y]);
        if ($grid[$y][$x] == "#") {
            $ngrid[$y][$x] = $nbugs == 1 ? "#" : ".";
        } else {
            $ngrid[$y][$x] = $nbugs == 1 || $nbugs == 2 ? "#" : ".";
        }
    }
    return $ngrid;
}

// ==================================================
// > PART 1
// ==================================================
$grid = array_map("str_split", explode("\n", (string) $input));
$seen = [];

while (true) {
    $grid = mutate($grid, function ($pos) use ($grid) {
        $nbugs = 0;
        foreach (neighbors($pos) as [$x, $y]) {
            if (($grid[$y][$x] ?? false) == "#") $nbugs++;
        }
        return $nbugs;
    });

    $state = implode("", array_merge(...$grid));
    if (!empty($seen[$state])) {
        $solution_1 = 0;
        foreach (str_split($state) as $i => $c) {
            if ($c == "#") $solution_1 += (2 ** $i);
        }
        break;
    }
    $seen[$state] = true;
}

// ==================================================
// > PART 2
// ==================================================
$grids = [array_map("str_split", explode("\n", (string) $input))];
$empty = array_chunk(array_fill(0, 25, "."), 5);

for ($m = 0; $m < 200; $m++) {
    // Expand up if first is not empty
    if (array_search("#", array_merge(...$grids[0])) !== false) {
        array_unshift($grids, $empty);
    }

    // Expand down if last is not empty
    if (array_search("#", array_merge(...$grids[count($grids) - 1])) !== false) {
        $grids[] = $empty;
    }

    // Mutate each level
    $ngrids = [];
    foreach ($grids as $i=>$grid) {
        $ngrids[$i] = mutate($grid, function ($pos) use ($grid, $i, $grids) {
            $outer = $grids[$i - 1] ?? false;
            $inner = $grids[$i + 1] ?? false;
            $nbugs = 0;

            foreach (neighbors($pos) as [$x, $y]) {
                if (isset($grid[$y][$x]) && ($x != 2 || $y != 2)) {
                    if ($grid[$y][$x] == "#") $nbugs++;
                } elseif ($x == 2 && $y == 2) {
                    if (empty($inner)) continue;
                    $nbugs = match (implode(";", $pos)) {
                        "2;1" => array_reduce(range(0, 4), fn ($n, $dx) => $n + ($inner[0][$dx] == "#" ? 1 : 0) , $nbugs),
                        "2;3" => array_reduce(range(0, 4), fn ($n, $dx) => $n + ($inner[4][$dx] == "#" ? 1 : 0) , $nbugs),
                        "1;2" => array_reduce(range(0, 4), fn ($n, $dy) => $n + ($inner[$dy][0] == "#" ? 1 : 0) , $nbugs),
                        "3;2" => array_reduce(range(0, 4), fn ($n, $dy) => $n + ($inner[$dy][4] == "#" ? 1 : 0) , $nbugs),
                    };
                } else {
                    if (empty($outer)) continue;
                    if ($y == -1 && $outer[1][2] == "#") $nbugs++;
                    if ($y == 5 && $outer[3][2] == "#") $nbugs++;
                    if ($x == -1 && $outer[2][1] == "#") $nbugs++;
                    if ($x == 5 && $outer[2][3] == "#") $nbugs++;
                }
            }

            return $nbugs;
        });
        $ngrids[$i][2][2] = "?";
    }
    $grids = $ngrids;
}

$solution_2 = count(array_diff(array_merge(...array_merge(...$grids)), ["?", "."]));