Subi um sistema em CodeIgniter 3.1.13 pro PHP 8.3 e ele morreu no primeiro request: Call to undefined function each(), com o trace terminando em system/core/Common.php. Esse arquivo não é o culpado. É nele que mora o _exception_handler, que o system/core/CodeIgniter.php pendura no boot com set_exception_handler(), então todo fatal passa por ali. Quem interessa é a linha de cima do trace, dentro de application/.
Os três pontos que quebram
O each() e o create_function() saíram no PHP 8.0 e não têm substituto automático: o primeiro vira foreach, o segundo vira closure. O terceiro é o traiçoeiro, porque não aparece no boot: desde o PHP 8.1 o mysqli reporta falha por exceção (MYSQLI_REPORT_ERROR e MYSQLI_REPORT_STRICT vêm ligados por padrão). O driver em system/database/drivers/mysqli/mysqli_driver.php devolve FALSE pro DB_driver.php tratar, e é esse contrato que some: a primeira query ruim estoura mysqli_sql_exception antes de qualquer tratamento do framework.
grep -rn --include='*.php' -E 'beach(|bcreate_function(' application/ system/
# varredura completa; o stable 9.3.4 é de 2019 e não conhece o 8.3
composer require --dev phpcompatibility/php-compatibility:dev-develop
dealerdirect/phpcodesniffer-composer-installer
vendor/bin/phpcs -p application/ --standard=PHPCompatibility
--runtime-set testVersion 8.3
O remendo que segura o mysqli
// index.php, antes do require_once BASEPATH.'core/CodeIgniter.php'
mysqli_report(MYSQLI_REPORT_OFF);
Isso devolve o comportamento antigo e faz o CI voltar a tratar erro de banco como ele sabe. É remendo: esconde a exceção, não conserta a query. Deixe o db_debug ligado em homologação pra não perder o erro de vista. Argumento opcional declarado antes de obrigatório é só Deprecated desde o 8.0, não derruba nada, mas em library de terceiro isso enche o error.log a cada requisição.
A conta honesta: o 3.1.13 é de 2022 e não vai ganhar versão nova. Se o phpcs voltar com centenas de ocorrências espalhadas em libraries que ninguém mantém, remendar sai mais caro que reescrever o módulo crítico. Rode as duas varreduras em homologação com display_errors ligado e navegue nas telas que usam banco: o que não quebra no boot costuma quebrar na primeira consulta.