Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CI4 + PHP 8.1: Finally block not executed when mysqli_sql_exception occurs (but works with register_shutdown_function)

I'm working on a CodeIgniter 4 project (CI: 4.4.3 production mode | PHP: 8.1.32), and I'm running into a strange issue where my finally block never executes if a database error happens during execution — even though I'm catching \Throwable (and doesn't remove the lock file).

But when I uncomment register_shutdown_function(), it works and cleans up the lock file.

Here's a minimal reproduction:

run() method in CronRunner

namespace App\Models;

class CronRunner extends CI_Model {

  public function run($data) {

    $result = "OK";

    $executedAt = date('Y-m-d H:i:s'); // for logs
    $executedAtUnix = time();         // for duration calc

    $lockFile = WRITEPATH . 'cache/cronRunner.lock';

    if (file_exists($lockFile))
      return;

    file_put_contents($lockFile, ENVIRONMENT);

    // This works — when it's enabled, the file gets removed
    // register_shutdown_function(function () use ($lockFile) {
    //   if (file_exists($lockFile)) {
    //     unlink($lockFile);
    //   }
    // });

    try {
      $this->notifyLongRunningTask();
    } catch (\Throwable $e2) {
      $result = $e2->getMessage();
      log_message('error', (string) $result);
    } finally {
      log_message('error', 'FINALLY BLOCK REACHED');
      if (file_exists($lockFile)) {
        unlink($lockFile);
      }
    }

    return ['result' => $result];
  }
}

Here’s notifyLongRunningTask()

function notifyLongRunningTask() {
  $PushSubscriptions = new PushSubscriptions();
  $PushSentLog = new PushSentLog();

  $db = \Config\Database::connect();

  $sql = "
    SELECT hs.id as hourly_id, date_started, time_started, hs.organization_id, hs.user_created, c.name Client, p.client_id
    FROM HourlySheet hs
      JOIN ToDo t ON hs.todo_id = t.id
      JOIN Project p ON t.project_id = p.id
      JOIN Client c ON p.client_id = c.id
    WHERE time_finished IS NULL
  ";

  $query = $db->query($sql);

  foreach ($query->getResult() as $row) {
    $startedAt = strtotime($row->date_started . ' ' . $row->time_started);
    $now = time();

    // This line causes the issue
    $PushSentLog_res = $PushSentLog->select(" AND user_created = {$row->user_created} AND name = 'notifyLongRunningTask' ORDER BY id DESC LIMIT 1 ");
    
    // ...
  }
}

The actual query error

mysqli_sql_exception: Column 'name' in WHERE is ambiguous

And the query is triggered from within the select() method of our CI_Model-based class. That method includes this logic:

if (!$query) {
  $error = $db->error()["message"];
  $baseClass = get_parent_class($this);
  $this->log_it($error, $baseClass . " " . __FUNCTION__ . ' DB Error');
  return ['result' => (string) $error];
}

So the SQL error is:

  • Caught
  • Logged
  • Returned as part of ['result' => '...']

But still — somehow — this causes PHP/CI4 to terminate execution before finally runs, and the lock file is not removed.


What works

If I uncomment the register_shutdown_function(), then the lock file is removed as expected.

This tells me the script is being shut down unexpectedly, despite \Throwable being caught, and no exit() or fatal visible.


Why is this happening?

  • We're on PHP 8.1.32 and CI4 4.4.3
  • DBDebug is set to false (production mode)
  • The query fails with a typical ambiguous column error
  • It is handled inside select() (custom function)
  • But the finally block from the run() method is never reached
  • The shutdown function does get called

Question

Why is the finally block not executed, even though the exception is handled and no uncaught fatal is shown?

Is this a PHP 8.1 + CI4 side effect?
Or are certain internal CI4 behaviors forcing a shutdown?

Any insight from CI4 internals or PHP changes in 8.1+ would be much appreciated.

like image 416
Dejan Dozet Avatar asked Nov 30 '25 10:11

Dejan Dozet


1 Answers

Why is the finally block not executed, even though the exception is handled and no uncaught fatal is shown?

Even though PHP promises finally blocks are always run, they do not execute if a fatal error or engine-level crash occurs ( out-of-memory, invalid internal op , destructor-triggered errors etc). Such type of errors bypass exception handling , and invoke script shutdown

Is this a PHP 8.1 + CI4 side effect?

This behaviour might be a CI4 database internals, after the exception is caught — particularly due to destructing queries ,invalid calls inside models or lazy connections It might also be from a PHP fatal error being triggered by a __destruct() method

if $query->getResult() was not valid or failed halfway Even after your select() method catches the query error, CI4 might still attempt to perform some additional follow up like result parsing, model hydration, that might lerads to fatal error inside __destruct() .

you can use register_shutdown_function() hook which is executed at script termination , this will run even if finally is skipped due to whatever.

you can add this to your run() method for a precheck

register_shutdown_function(function () use ($lockFile) {
    $error = error_get_last();
    if ($error !== null) {
        log_message('critical', 'fatal error: ' . print_r($error, true));
    }
});

Keep this as a fallback —

methods like $PushSentLog->select(...) ... can be placed in their own try catch block to better get the exceptions.

Also please take care you are not breaking CI's internal SQL compiler or query builder .CI4's Model::select() expects field names, not raw WHERE conditions. If you’re doing:

$PushSentLog->where('model_created', $row->model_created)
            ->where('name', 'notifyLongRunningTask')
            ->orderBy('id', 'DESC')
            ->limit(1)
            ->find();
like image 103
TanvirChowdhury Avatar answered Dec 03 '25 00:12

TanvirChowdhury



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!