I've never had my code reviewed before, so I am taking this opportunity to see what can be improved with my coding style. I am still somewhat "new" to PHP, in the sense that I'm an old dog finally learning new tricks.
Here is a MySQLi wrapper I've coded in PHP. Overall, this code is part of an API project. I'm making an API in PHP which I will use for my WPF Desktop Application for the Database Models.
Here is Class.Config.php
:
<?php
namespace Database;
use Exceptions\DatabaseException;
use Exceptions\ConnectionException;
class Config
{
/**
* @var string $host The connection host.
* @var string $username The connection username.
* @var string $password The connection password.
* @var string $dbname The connection database name.
*/
private string $host, $username, $password, $dbname;
/**
* @var \mysqli $conn The mysqli connection object.
*/
private \mysqli $conn;
/**
* Configures the Config object with the default connection string.
*
* @param array $config The config array, expects indexes of: host, username, password, and dbname.
* @param mysqli $connection The optional mysqli connection object.
*/
public function __construct(?array $config = null, \mysqli $connection = null)
{
if ($connection !== null && !($connection instanceof \mysqli))
{
throw new \InvalidArgumentException(
"Invalid mysqli connection object passed. Instance of 'mysqli' expected, '" . gettype($connection) . "' given."
);
}
if ($config === null)
{
// Retrieve an array of the connection data from the database.ini file.
$config = parse_ini_file("database.ini");
}
// Retrieve the database credentials from the parsed file
foreach($config as $key => $value)
{
$this->$key = $value ?? null;
}
// Verify that the config file properly sets up the variables.
try
{
if (!isset($this->host, $this->username, $this->password, $this->dbname)
|| empty($this->host)
|| empty($this->username)
|| empty($this->password)
|| empty($this->dbname))
{
throw new \InvalidArgumentException("Connection string expects values for ['host', 'username', 'password', 'dbname']");
}
}
catch (\InvalidArgumentException $e)
{
error_log("Missing Configuration Index: {$e}");
throw $e;
}
// Create a new MySQLi connection
$this->conn =
$connection ??
new \mysqli(
$this->host,
$this->username,
$this->password,
$this->dbname
);
}
/**
* Validates the connection.
*
* @return mysqli The mysqli connection object.
*/
public function connect()
{
try
{
/**
* Checks to see if there are any errors with the validated connection string.
*
* @var \mysqli $this->conn The connection object configured in the constructor.
*/
if ($this->conn->connect_error)
{
throw new ConnectionException(
$this->conn->connect_error
);
}
else
{
return $this->conn;
}
}
catch (ConnectionException $e)
{
error_log("Connection Error: " . $e->getMessage());
throw $e;
}
catch (DatabaseException $e)
{
error_log("Database Error: " . $e->getMessage());
throw $e;
}
catch (Exception $e)
{
error_log("Error: " . $e->getMessage());
throw $e;
}
}
}
?>
Here is class.MySQLi.php
:
<?php
namespace Database;
use Database\Config;
class MySQLi
{
/**
* @var \mysqli $conn The mysqli connection object.
*/
private \mysqli $conn;
/**
* Initialize the connection between the database config.
*/
public function __construct()
{
$conn = new Config;
$this->conn = $conn->connect();
}
/**
* Method for getting the connection variable.
*
* @return \mysqli | bool $this->conn The mysqli object configured in the constructor.
*/
public function getConnection() : \mysqli | bool
{
return $this->conn;
}
/**
* Close the connection once the script ends.
*/
public function __destruct()
{
$this->conn->close();
}
/**
* Build a parameterized query to protect against SQL injections by default.
*
* @param string $query The SQL query string with placeholders for parameters.
* @param mixed ...$params The array? of parameters to be bound to the query.
* @return \mysqli_result|bool Returns the result of the query or false if an error is encountered.
*/
public function Query(string $query, ...$params) : \mysqli_result | bool
{
// Prepare the SQL statement or trigger an error if preparation fails.
$statement = $this->getConnection()->prepare($query) or trigger_error($this->getConnection()->error, E_USER_ERROR);
if (!$statement) return false;
// Build the parameters if provided.
if(!empty($params))
{
$types = [];
$bindParams = [];
foreach ($params as $param)
{
// Get the type of each parameter: i, d, s, or b.
$types[] = $this->getParamType($param);
$bindParams[] = $param;
}
try
{
$types = implode("", $types);
$statement->bind_param($types, ...$bindParams);
}
catch (\ArgumentCountError $e)
{
return false;
}
}
// Execute the statement and return the result if successful
if ($statement->execute())
{
$result = $statement->get_result();
$statement->close();
return $result;
}
return false;
}
/**
* Insert rows of data into a database table.
*
* @param string $query The SQL query string with placeholders for parameters.
* @param mixed ...$params The array? of parameters to be bound to the query.
*/
public function Insert(string $query, ...$params)
{
// Still debating usage.
}
/**
* Fetch rows of data from a database table.
*
* @param string $query The SQL query string with placeholders for parameters.
* @param mixed ...$params The array? of parameters to be bound to the query.
* @return array|false Returns an array of rows on success, or false on failure.
*/
public function Rows(string $query, ...$params) : array | bool
{
if($result = $this->Query($query, ...$params))
{
$rows = [];
while ($row = $result->fetch_assoc())
{
$rows[] = $row;
}
return $rows;
}
return false;
}
/**
* Get the parameter type for binding in a prepared statement.
*
* @param mixed $param The parameter value.
* @return string Returns the parameter type character.
*/
private function getParamType($param) : string
{
if (is_int($param))
{
return "i";
}
elseif (is_float($param))
{
return "d";
}
elseif (is_string($param))
{
return "s";
}
else
{
return "b";
}
}
}
?>
Usage
Simplify Fetching Rows From Database
With my wrapper class, I can simply do this:
// Get the database connection
$conn = new MySQLi;
// SQL query to select all data from the table
$query = "SELECT * FROM structure_ranks";
// Fetch all rows from the result set and store them in the array
$rows = $conn->Rows($query);
// Output the array as JSON
echo json_encode($rows, JSON_PRETTY_PRINT);
As opposed to doing this:
// Create an instance of the Config class
$config = new Config();
// Get the database connection
$conn = $config->connect();
// Check the connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// SQL query to select all data from the table
$sql = "SELECT * FROM structure_ranks";
// Execute the query
$result = $conn->query($sql);
// Fetch all rows from the result set and store them in the array
$array = [];
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$array[$row['id']] = $row;
}
}
// Close the connection
$conn->close();
// Output the array as JSON
echo json_encode($array, JSON_PRETTY_PRINT);
$conn
variable in several places. In your code$conn
can be an instantiation of themysqli
class, or an instantiation of theConfig
class. So, what does "conn" really mean? Oh, and you also have a$connection
variable. I'm confused. I think it is good practice when a variable name uniquely describes what it contains. Same is true for class names. YourConfig
class actually makes the database connection and yourMySQLi
class queries it. Those were the best names you could come up with? \$\endgroup\$$conn
variable returns this:// Create a new MySQLi connection $this->conn = $connection ?? new \mysqli($this->host, $this->username, $this->password, $this->dbname);
, which is from the constructor of the Config class. So,$conn = new Config;
just means$conn = new mysqli(...)
The config class could've been called something else, but it just sets up and validates the mysqli connection. That one ofconnection
variable was my attempt to say "this connection variable is from the outside, different from everything else going on here" \$\endgroup\$$conn = new Config();
isn't the same as$conn = new mysqli()
or even$conn = new MySQLi()
. The confusion seems real. Anyway, all I wanted to say is that your "naming of things" needs some work, the details are less important. Here's a bit of a backgrounder. Just think what will happen when you need to configure something else? Will you use classConfig2
? \$\endgroup\$