Skip to content

Main connection [MySQLi]: mysqli::real_connect(): Argument #5 ($port) must be of type ?int, string given #9611

@MarlonRaphael

Description

@MarlonRaphael

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:

  1. Applying the (int) cast in the main app/Config/Database.php.
  2. Clearing the CodeIgniter cache (php spark cache:clear).
  3. 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

  1. Set up a CodeIgniter 4.5+ project with PHP 8.1 or higher.
  2. In the .env file, set CI_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
  1. 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),
    ];
  1. 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
        ];
    }
  1. Create a route and a controller method that attempts to perform any database query.
  2. Access the route in a browser. The application will throw the TypeError because the development 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:

  1. More Robust Type Handling: The framework's database connection handler could proactively cast the $port property to an integer before calling mysqli::real_connect(). This would make the connection process resilient to string values coming from any configuration source.

  2. 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?

Image

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,
        ];
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions