Following my previous question Database interface and PDO adapter, I wrote a new simple class to connect to Database and run a query returning a PDO statement.
<?php
namespace System\Database;
use \PDO;
/**
* Database connection class
*/
class DB
{
protected $pdo;
/**
* Connect to database
* @param array $config
*/
public function __construct(array $dbConfig)
{
$connStr = 'mysql:host='.$dbConfig['server'].';port='.$dbConfig['port'].';dbname='.$dbConfig['name'].';charset='.$dbConfig['charset'];
try{
$this->pdo = new PDO(
$connStr,
$dbConfig['username'],
$dbConfig['password']
);
# We can now log any exceptions on Fatal error.
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
# Disable emulation of prepared statements, use REAL prepared statements instead.
$this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
return true;
// Error handling
}catch(PDOException $e){
throw new \Exception("Failed to connect to DB: ". $e->getMessage(), 1);
}
}
/**
* Execute a $sql query with or without params. If $params is Multidimensional array bind variables passed by
* @param string $sql Query to execute
* @param array $params Params
* @return PDOStatement
*/
public function run(string $sql, array $params = [])
{
if(empty($params)){
return $this->pdo->query($sql);
}
$stmt = $this->pdo->prepare($sql);
if(is_array($params[0])){
foreach ($params as $param) {
$stmt->bindParam(
$param['name'],
$param['value'],
$param['pdoType'],
$param['length']
);
}
$stmt->execute();
}else{
$stmt->execute($params);
}
return $stmt;
}
/**
* Returns the last inserted id
* @return int ID
*/
public function lastInsertId(): int
{
return $this->pdo->lastInsertId();
}
/**
* Close the connection
*/
public function closeConnection()
{
$this->pdo = null;
}
}
?>
After that I'm writing a data mapper class as follow:
<?php
namespace System\Database;
use System\Database\DB;
use \PDO;
/**
*
*/
abstract class DataMapper
{
protected $db;
// name of the table
protected $table;
// table columns with info
protected $_fields = array();
// primary key
protected $_primary;
// flag if primary key is Auto Increment
protected $_primaryAI = false;
// protected $fillable = [];
protected $_pdoBindTypes = [
'char' => PDO::PARAM_STR,
'int' => PDO::PARAM_INT,
'bool' => PDO::PARAM_BOOL,
'date' => PDO::PARAM_STR,
'time' => PDO::PARAM_INT,
'text' => PDO::PARAM_STR,
'blob' => PDO::PARAM_LOB,
'binary' => PDO::PARAM_LOB
];
public function __construct(DB $db)
{
$this->db = $db;
if(!empty($this->table))
{
$this->getTableFields();
}
}
/**
* Get column list form table
* [internal function]
*/
protected function getTableFields()
{
// Fetch all columns and store in $this->_fields
$fields = $this->db->run("DESCRIBE $this->table")->fetchAll(PDO::FETCH_ASSOC);
foreach($fields as $field){
$this->_fields[$field['Field']] = $this->parseColumnType($field['Type']);
if($field['Key'] == 'PRI'){
$this->_primary = $field['Field'];
$this->_primaryAI = ($field['Extra'] == 'auto_increment');
}
}
}
/**
* Parse PDO-produced column type
* [internal function]
*/
protected function parseColumnType($colType)
{
$colInfo = array();
$colParts = explode(" ", $colType);
if($fparen = strpos($colParts[0], "("))
{
$colInfo['type'] = substr($colParts[0], 0, $fparen);
$colInfo['length'] = (int)str_replace(")", "", substr($colParts[0], $fparen+1));
$colInfo['attributes'] = isset($colParts[1]) ? $colParts[1] : NULL;
}
else
{
$colInfo['type'] = $colParts[0];
}
// PDO Bind types
$pdoType = '';
foreach($this->_pdoBindTypes as $pKey => $pType)
{
if(strpos(' '.strtolower($colInfo['type']).' ', $pKey)) {
$colInfo['pdoType'] = $pType;
break;
} else {
$colInfo['pdoType'] = PDO::PARAM_STR;
}
}
return $colInfo;
}
/**
* Get info of the column
* [internal function]
* @param string $column Column name
* @return array Type and lenght
*/
protected function getColumnType(string $column): array
{
return [
'pdoType' => $this->_fields[$column]['pdoType'],
'length' => isset($this->_fields[$column]['length']) ?: NULL,
];
}
/**
* Return array with all info for binding
* [internal function]
* @param string $column column name
* @param [type] $value value
* @return array
*/
protected function bindColumnType(string $column, $value): array
{
return array_merge(
[
'name' => ":{$column}",
'value' => $value,
],
$this->getColumnType($column)
);
}
/**
* Find a record by id and return a row
* @param [type] $id
* @param [type] $mode PDO Fetch Mode
* @return [type] row
*/
public function findById($id, $mode = PDO::FETCH_ASSOC)
{
// TODO: if not setted, throw exception
if(empty($this->table) or empty($this->_primary)) return false;
$pk = $this->_primary;
return $this->db
->run("SELECT * FROM $this->table WHERE $pk = :$pk", [$this->bindColumnType($pk, $id)])
->fetch($mode);
}
/**
* Insert a new record
* @param array $data data to insert [field => value]
*/
public function insert($data)
{
$fieldsStr = '';
$valuesStr = '';
$dataValues = array();
// generate sql query
foreach ($data as $f => $v) {
// check if exist the column name
if(array_key_exists($f, $this->_fields))
{
// if there is a primary key field in $data and it's "auto increment", skip
if($f == $this->_primary and $this->_primaryAI) continue;
$fieldsStr .= $f.',';
$valuesStr .= ":{$f},";
$dataValues[] = $this->bindColumnType($f, $v);
}
}
// remove last ,
$fieldsStr = substr($fieldsStr, 0, -1);
// remove last ,
$valuesStr = substr($valuesStr, 0, -1);
$this->db->run("INSERT INTO {$this->table} ({$fieldsStr}) VALUES ({$valuesStr})", $dataValues);
}
//TODO: update, delete
}
?>
I'm on the right way? Is It correct to get the list of table columns from the db to check the fields to be entered? Or is it better declare them in the child class like this:
<?php
namespace App;
use System\Database\DataMapper;
/**
*
*/
class User extends DataMapper
{
protected $table = 'users';
protected $fillable = [
'firstname' => 'string:20',
'lastname' => 'string:20',
'age' => 'int:3',
'email' => 'email',
];
public function test(){
// do something
}
}
?>
In the data mapper I should parse these informations to set the correct type and lenght for each field.
DataMapper::insert($data)
how is$this->table
ever given a value? Then at the end it says: "TODO: update, delete". These are not unimportant things. Are we looking at untested code? \$\endgroup\$update
anddelete
must be implemented yet. DataMapper isn't a static class, It must be extended to use It \$\endgroup\$