-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Description
PHP Version
8.3
CodeIgniter4 Version
4.6.1
CodeIgniter4 Installation Method
Composer (as dependency to an existing project)
Which operating systems have you tested for this bug?
Linux
Which server did you use?
fpm-fcgi
Database
MySQL 8.0.36
What happened?
After upgrading a project to CodeIgniter 4.5+ (which requires PHP 8.1+), the database connection started failing with a TypeError
.
Error Message:
CodeIgniter\Database\Exceptions\DatabaseException: Unable to connect to the database. Main connection [MySQLi]: mysqli::real_connect(): Argument #5 ($port) must be of type ?int, string given
The initial and correct solution was to apply an (int) cast to the port value in app/Config/Database.php
:
'port' => (int) env('database.default.port', 3306),
However, the error persisted even after:
- Applying the (int) cast in the main app/Config/Database.php.
- Clearing the CodeIgniter cache (php spark cache:clear).
- Restarting the PHP-FPM service to clear any Opcache.
The root cause was identified as an environment-specific configuration file
(app/Config/development/Database.php
) that was created in a previous version of the framework and did not have the (int)
cast. This file was silently overriding the main configuration.
While this is the expected behavior of the cascading configuration system, the debugging experience is very difficult. The error message gives no indication that the configuration is being overridden, leading the developer to believe the issue is in the main config file or a caching problem. This can cause significant lost time during upgrades.
Steps to Reproduce
- Set up a CodeIgniter 4.5+ project with PHP 8.1 or higher.
- In the
.env
file, setCI_ENVIRONMENT = development
and configure the default database connection details.
CI_ENVIRONMENT = development
database.default.hostname = localhost
database.default.database = ci4_test
database.default.username = root
database.default.password = root
database.default.DBDriver = MySQLi
database.default.port = 3306
- In the main
app/Config/Database.php
, correctly cast the port to an integer.
// in app/Config/Database.php
public array $default = [
// ...
'port' => (int) env('database.default.port', 3306),
];
- Configuration file at
app/Config/Database.php
. In this file, define the port without the integer cast, simulating an old configuration file.
<?php
namespace Config;
class Database extends \Config\Database
{
public array $default = [
'hostname' => 'localhost',
'username' => 'root',
'password' => 'root',
'database' => 'ci4_test',
'DBDriver' => 'MySQLi',
'port' => 3306, // Note: No (int) cast, or could be '3306' as a string
];
}
- Create a route and a controller method that attempts to perform any database query.
- Access the route in a browser. The application will throw the
TypeError
because thedevelopment
configuration file overrides the main one, and its port value is treated as a string.
Expected Output
There are two potential improvements for a better developer experience:
-
More Robust Type Handling: The framework's database connection handler could proactively cast the
$port
property to an integer before callingmysqli::real_connect()
. This would make the connection process resilient to string values coming from any configuration source. -
A More Informative Exception: If proactive casting is not desired, the
DatabaseException
could be enhanced to guide the developer. Instead of a generic "Unable to connect" message, it could be:
Unable to connect to the database. The port was provided as a string but an integer is required. Please ensure the port is an integer in all configuration files, including environment-specific ones (e.g., app/Config/development/Database.php).
This would immediately point the developer to the correct solution, acknowledging the cascading configuration system as a potential source of the issue.Thank you for considering this improvement to the developer experience.
Anything else?
app/Config/Database.php
<?php
namespace Config;
use CodeIgniter\Database\Config;
/**
* Database Configuration
*/
class Database extends Config
{
/**
* The directory that holds the Migrations and Seeds directories.
*/
public string $filesPath = APPPATH . 'Database' . DIRECTORY_SEPARATOR;
/**
* Lets you choose which connection group to use if no other is specified.
*/
public string $defaultGroup = 'default';
/**
* The default database connection.
*
* @var array<string, mixed>
*/
public array $default;
/**
* This database connection is used when
* running PHPUnit database tests.
*/
public array $tests;
public function __construct()
{
parent::__construct();
// Ensure that we always set the database group to 'tests' if
// we are currently running an automated test suite, so that
// we don't overwrite live data on accident.
if (ENVIRONMENT === 'testing') {
$this->defaultGroup = 'tests';
}
$this->default = [
'DSN' => '',
'hostname' => env('database.default.hostname', 'ci4_test'),
'username' => env('database.default.username', 'root'),
'password' => env('database.default.password', 'root'),
'database' => env('database.default.database', 'ci4_database'),
'DBDriver' => env('database.default.DBDriver', 'MySQLi'),
'DBPrefix' => env('database.default.DBPrefix', ''),
'pConnect' => false,
'DBDebug' => (ENVIRONMENT !== 'production'),
'cacheOn' => false,
'cacheDir' => '',
'charset' => 'utf8',
'DBCollat' => 'utf8_general_ci',
'swapPre' => '',
'encrypt' => false,
'compress' => false,
'strictOn' => false,
'failover' => [],
'port' => (int) 3306,
];
$this->tests = [
'DSN' => '',
'hostname' => env('database.tests.hostname', 'localhost'),
'username' => env('database.tests.username', 'root'),
'password' => env('database.tests.password', 'root'),
'database' => env('database.tests.database', ':memory:'),
'DBDriver' => env('database.tests.dbdriver', 'SQLite3'),
'DBPrefix' => 'db_',
'cacheOn' => false,
'cacheDir' => '',
'pConnect' => false,
'DBDebug' => true,
'charset' => 'utf8',
'DBCollat' => 'utf8_general_ci',
'swapPre' => '',
'encrypt' => false,
'compress' => false,
'strictOn' => false,
'failover' => [],
'port' => (int) env('database.tests.port', 3306),
'foreignKeys' => true,
'busyTimeout' => 1000,
];
}
}