I'm using a class for database connection, this class holds the CRUD methods, and also the database connection methods.
It's basically the core of all other classes that uses the DB.
I'm currently facing a problem on dealing with duplicate entry in unique columns, when inserting a record.
Database.class.php insertDB method:
class Database {
(...)
public function insertDB($sql,$params=null){
$con=$this->connect();
$query=$con->prepare($sql);
$query->execute($params);
$rs = $con->lastInsertId();
return $rs;
self::__destruct();
}
(...)
}
There is another class, PersonDAO.class.php, which is a class that inherits Database.class.
PersonDAO.class.php has its own method for inserting a record, which finally uses Database.class.php InsertBD method.
PersonDAO.class.php insert method: (notice it calls insertDB at the end).
class PersonDAO extends Database{
(...)
public function insert ($fields, $params=null) {
$numparams="";
for($i=0;$i<count($params);$i++) $numparams .= ",?";
$numparams=substr($numparams, 1);
$sql = "INSERT INTO Person ($fields) VALUES ($numparams)";
$t=$this->insertDB($sql,$params);
return $t;
}
(...)
}
The problem occurs in the registration form register.php, when I instantiate PersonDAO and use its insert method for inserting a duplicate entry into a column set as unique.
(...)
$person= new PersonDAO();
$fields="email,name";
$email="john#doe.com"; //already existing record. email column set as unique
$name="John Doe";
$params=array($email,$name);
try {
$rs = $person->insert($fields,$params);
} catch (PDOException $e) {
if ($e->errorInfo[1] == 1062) {
echo "Cannot insert record. Duplicate entry";
}
}
(...)
It's not catching duplicate entry exception, as if there was no errors.
var_dump($rs) contains:
string(1) "0"
But should not it catch PDOException and print "Cannot insert record. Duplicate entry"?
Is there any "Uncaught Exception ..." message ? Perhaps, You first need to catch the Exception in your Database.class.php insertDB method and then throw it.
class Database {
(...)
public function insertDB($sql,$params=null){
try{
$con=$this->connect();
$query=$con->prepare($sql);
$query->execute($params);
$rs = $con->lastInsertId();
return $rs;
self::__destruct();
}
catch(Exception $e){
throw $e;
}
}
(...)
}
Or if there is no error at all. Check
$con->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
Related
I have run into an unexpected fatal error while testing an app that uses my database utility class (see code below). Here is a rundown of the test that was performed:
a new product was added to the database
the new product was put on an order
an attempt was made to delete the new product (while it was still being used on an order)
Results:
as expected, the foreign key constraint in the database prevented deletion of the product
unexpectedly, the following fatal error was displayed - Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[23000]: [Microsoft][SQL Server Native Client 11.0][SQL Server]The DELETE statement conflicted with the REFERENCE constraint "FK_Orders_Products". The conflict occurred in database "AppTEST", table "dbo.Orders", column 'productID'.' in C:\xampp\htdocs\App\classes\utility\DB.php:140 Stack trace: ...Line 140 is $stmt->execute();, which is in the query() method in the code below.
Forum posts describing similar errors called out prepending global classes with a backslash, which I did, but to no avail. Why is the fatal error occurring?
Here are the pertinent parts of my database utility class for this question:
namespace classes\utility;
use \PDO,
\Exception,
\PDOException;
class DB
{
private $_pdo, # stores PDO object when it's instantiated
$_error, # stores whether query failed or not
$_results, # stores dataset returned by query
$_count = 0; # stores number of data rows returned
/*connect to database*/
private function __construct()
{
try
{
$connection = new \PDO('sqlsrv:Server=' . DB_HOST . ';Database=' . DB_NAME);
$connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); //to capture error messages returned by db
$this->setPdo($connection);
}
catch(\PDOException $e)
{
exit((DEBUGMODE) ? $e->getMessage() : 'There was a problem connecting to the database.');
}
}
# generic query method - uses named placeholders for parameter binding
private function query($sql, $parameters = [])
{
try
{
$this->setError(false);
$stmt = $this->getPdo()->prepare($sql); //assign to a variable with a short name
if ($stmt):
if (count($parameters)):
foreach($parameters as $name => $parameter):
$stmt->bindValue(':'.$name, $parameter);
endforeach;
endif;
$stmt->execute();
$this->setCount($stmt->rowCount());
if ($stmt->rowCount() == -1): //conditionally do the following if SQL stmt was a SELECT
$this->setResults($stmt->fetchAll(\PDO::FETCH_OBJ));
$this->setCount($stmt->rowCount()); //overwrite count attribute with number of rows returned by SELECT stmt
endif;
endif;
return $this;
}
catch (\PDOException $e)
{
$this->setError(true);
throw $e;
}
}
// setter and getter methods for private variables not shown
}
EDIT:
Here is the code that calls the query() method. This method is also part of the DB class.
public function runQuery($operation, $sql, $parameters)
{
try
{
$this->query($sql, $parameters); //call query method all $operation types (read, write)
if ($operation == 'read'):
if ($this->getCount()):
if ($this->getCount() > 1): //for "read all"
$data = $this->getResults();
else: //for "read one"
$data = $this->getFirstResult();
endif;
$data = Arr::objectToArray($data);
return $data;
else:
throw new \Exception("No data to display."); //throw an exception if query runs fine but returns zero rows
endif;
endif;
}
catch (\PDOException $e)
{
throw (DEBUGMODE) ? $e : new \Exception("A database error occurred. The $operation operation failed.");
}
}
The problem turned out to be missing a backslash in a calling method. I had prepended the backslash in all references to global classes in the DB class, but not in other classes in other namespaces that had catch blocks.
There were actually 2 different ways to resolve this problem.
One approach was to change catch (Exception $e) to catch (\Exception $e). Alternatively, leaving catch (Exception $e) as-is throughout the code and adding use \Exception; after the namespace declaration and before the class definition worked as well.
suppose I have the class named as User and user is an object of that class,
now I call a function named as storevalues from user(an object of class User).
In the storevalues function,__construct function is called which assign the value to the class member.$this->name,$this->email,$this->password.
finally I try to store these values in DATABASE through PDO.
$conn = new PDO(DB_DSN,DB_USERNAME,DB_PASSWORD);
$sql="insert into user_info(name,email,password)values(:name,:email,:password)";
$st=$conn->prepare($sql);
$st->bindValue(":name",$this->name,PDO::PARAM_STR);
$st->bindValue(":email",$ths->email,PDO::PARAM_STR);
$st->bindValue(":password",$this->password,PDO::PARAM_STR);
$st->execute();
But the above code is not working.The connection is successfull made to the database but query is not executed.I want to know what mistake I have done in this code.
When I try assigning the class members value to the new variable then it works.The code below shows that method
$name=$this->name;
$email=$this->email;
$password=$this->password;
$conn = new PDO(DB_DSN,DB_USERNAME,DB_PASSWORD);
$sql="insert into user_info(name,email,password)values(:name,:email,:password)";
$st=$conn->prepare($sql);
$st->bindParam(":name",$name,PDO::PARAM_STR);
$st->bindParam(":email",$email,PDO::PARAM_STR);
$st->bindParam(":password",$password,PDO::PARAM_STR);
$st->execute();
I am a beginner in php and pdo and I know that my code is inefficient.Help me in finding the mistake in first method and identifying my mistakes.
User class
class User
{
public $name=null;
public $email=null;
public $password=null;
public function __construct($data=array())
{
if(isset($data['name']))
$this->name=$data['name'];
if(isset($data['email']))
$this->email=$data['email'];
if(isset($data['password']))
$this->password=$data['password'];
}
public function storevalues($result=array())
{
$this->__construct($result);
$conn = new PDO(DB_DSN,DB_USERNAME,DB_PASSWORD);
$sql="insert into user_info(name,email,password)values(:name,:email,:password)";
$st=$conn->prepare($sql);
$st->bindParam(":name",$this->name,PDO::PARAM_STR);
$st->bindParam(":email",$this->email,PDO::PARAM_STR);
$st->bindParam(":password",$this->password,PDO::PARAM_STR);
$st->execute();
}
}
You may catch the SQL error after the execute:
if ( ! $st->execute) {
Throw new exception('Mysql Error - ' . implode(',', $st->errorInfo()));
}
We are using propel as orm on a mysql db.
We are changing several tables and call external services during a transaction. In between we log these actions and the response in a logging table. If an error occurs, we revert the actions but want to keep the log messages. At the moment the log messages are using the same transaction scope and would be reverted with the transaction.
Do I get a new connection and transactionscope with
$con = Propel::getConnection(DATABASE_NAME);
or do I have to check if the same connection is returned
PSEUDO CODE
public function write_log()
{
$con = Propel::getConnection(DATABASE_NAME);
$log=new Log();
$log->message('foo');
$log->save($con);
}
public function change_data()
{
write_log('start');
$con = Propel::getConnection(DATABASE_NAME);
$con->beginTransaction();
try {
//this message should stay in the database
write_log('change_db_data:'.$new_db_value);
//this should be reverted
change_db_data($new_db_value);
write_log('call webservice_1');
$response=call_webservice_1();
write_log($response);
if($response==null)
{
$con->rollback();
}
write_log('call webservice_2');
$response=call_webservice_2();
write_log($response);
if($response==null)
{
$con->rollback();
}
$con->commit();
}
catch(Exception $e){
$con->rollback();
write_log('error')
}
write_log('end');
}
Good choice picking Propel. You have two choices, either encapsulate the logging in their own transactions, or use a nested transaction, uniquely supported by Propel.
The first requires only transactionality in the write_log function:
public function write_log()
{
$con = Propel::getConnection(DATABASE_NAME);
$con->beginTransaction();
$log=new Log();
$log->message('foo');
$log->save($con);
$con->commit();
}
The second is to start a nested transaction and ensure that only the inner transaction is rolled back:
public function write_log()
{
$con = Propel::getConnection(DATABASE_NAME);
$log=new Log();
$log->message('foo');
$log->save($con);
}
public function change_data()
{
$con = Propel::getConnection(DATABASE_NAME);
$con->beginTransaction();
write_log('start');
$con->beginTransaction();
try {
//this message should stay in the database
write_log('change_db_data:'.$new_db_value);
//this should be reverted
change_db_data($new_db_value);
write_log('call webservice_1');
$response=call_webservice_1();
write_log($response);
if($response==null)
{
throw new \Exception('Null response.');
}
write_log('call webservice_2');
$response=call_webservice_2();
write_log($response);
if($response==null)
{
throw new \Exception('Null response.');
}
$con->commit();
}
catch(Exception $e){
$con->rollback();
write_log('error')
}
write_log('end');
$con->commit();
}
I have had no formal teaching in coding and was hoping if anyone could tell me if I was being to cautious with my code?
// Insert info to the db
if ($stmt = $db_connect->prepare("INSERT INTO db (col1, col2) VALUES (?, ?)")) {
if(!$stmt->execute([$val1, $val2])) {
exit("1:Faild to create deal");
}
// Get last id
$id = (int)$db_connect->lastInsertId();
$stmt->closeCursor();
} else { exit("0:Faild to create deal"); }
// Create the folder
if(!mkdir("folder/folder".$id)) {
if($stmt = $db_connect->prepare("DELETE FROM db WHERE id=?")) {
if(!$stmt->execute([$id])) {
exit("1:Faild to create the directory -> Faild to remove the row from the database");
}
exit("Faild to create the directory");
}
exit("0:Faild to create the directory -> Faild to remove the row from the database");
}
I repeat the create folder statement 2 more times with the same layout. It's just repeatable code that looks to be overkill.
Note: The package I have with my host only has MyISAM tables so I can't use Rollback.
If something fails I want to undo everything that has passed.
Could someone please give me some guidance to best practices or am I doing it right?
i re-structured and extended your code plus added a bit of simple error handling by using exceptions.
first you should set your PDO error handling to exception mode:
$db_connect->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
then i have capsulated your actions into functions, which you can put into a separate file and include it, or even nest them into classes:
/*** FUNCTIONS ***/
// insert info to the db
function dbInsertInfo($db_connect, $val1, $val2)
{
$stmt = $db_connect->prepare("INSERT INTO db (col1, col2) VALUES (?, ?)");
$stmt->execute([$val1, $val2]));
}
//-------------------------------
// get the last insert id
function dbGetId($db_connect)
{
$id = (int)$db_connect->lastInsertId();
$stmt->closeCursor();
return $id;
}
//-------------------------------
// delete db-entry
function dbDeleteId($db_connect, $id)
{
$stmt = $db_connect->prepare("DELETE FROM db WHERE id=?");
$stmt->execute([$id]);
}
//-------------------------------
// create the folder
function createFolder($id)
{
if(!mkdir("folder/folder".$id)) throw new Exception("Failed to create the directory");
}
//-------------------------------
then here is your procedure with all the try{ } catch{ } sections for the error handling by exceptions:
/* PROCEDURE */
// 01 | try to insert into db
try
{
dbInsertInfo($db_connect, $val1, $val2);
}
catch(PDOException $e)
{
//if exception thrown, do not continue the script:
echo "Unable to insert into DB: ".$e->getMessage();
exit();
}
//-------------------------------
// 02 | try to get last insert id
$id = false;
try
{
$id = dbGetId($db_connect);
}
catch(PDOException $e)
{
//if exception thrown, do not continue the script:
echo "Unable to get last insert id from DB: ".$e->getMessage();
exit();
}
//-------------------------------
// 03 | try to create folder // if it fails -> try to delete db entry
try
{
createFolder($id);
}
catch(Exception $e)
{
// if exception caught, try to remove the corresponding DB entry:
echo $e->getMessage();
echo "<br />";
echo "trying to remove DB entry now";
// try to delete db entry
try
{
dbDeleteId($db_connect, $id);
}
catch(PDOException $e)
{
//if exception thrown, do not continue the script:
echo "Unable to delete from DB: ".$e->getMessage();
exit();
}
}
//-------------------------------
/* Everything worked fine if you get to this point of the code*/
just might seem like an technical overkill for you now, but i think it's much more structured and better to read it, once you got into it. plus, it's only divided into 3 steps.
Following up on the post here, it seems that I have managed to extend the pdo class,
class database_extended extends PDO
{
#make a connection
public function __construct($dsn,$username,$password)
{
try
{
parent::__construct($dsn,$username,$password);
//$this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
catch (PDOException $e)
{
# call the get_error function
self::get_error($e);
}
}
#get the number of rows in a result
public function num_rows($query)
{
try
{
# create a prepared statement
$stmt = parent::prepare($query);
# execute query
$stmt->execute();
# return the result
return $stmt->rowCount();
}
catch (PDOException $e)
{
# call the get_error function
self::get_error($e);
}
}
# display error
public function get_error($e)
{
$this->connection = null;
die($e->getMessage());
}
# closes the database connection when object is destroyed.
public function __destruct()
{
}
}
But it seems not quite right - I tested the num_rows method with a mistake in the query on purpose, so this method can return an error message,
# the host used to access DB
define('DB_HOST', 'localhost');
# the username used to access DB
define('DB_USER', 'root');
# the password for the username
define('DB_PASS', 'xx');
# the name of your databse
define('DB_NAME', 'xx_2011');
# the data source name
define('DSN', 'mysql:dbname='.DB_NAME.';host='.DB_HOST);
include 'class_database.php';
$connection = new database_extended(DSN,DB_USER,DB_PASS);
$sql = "
SELECT *
FROM table_not_exist
ORDER BY cnt_id DESC
";
echo $connection->num_rows($sql);
It should returns,
SQLSTATE[42S02]: Base table or view
not found: 1146 Table
'xx_2011.table_not_exist' doesn't
exist
But it returns a 0 instead! Why?? How can I fix it?
Thanks.
Because PDOStatement::execute does not throw, it only returns false upon failure. Hence your code never enters the catch block. Should be more along the lines of this:
$stmt = parent::prepare($query);
if (!$stmt->execute()) {
self::get_error();
}
return $stmt->rowCount();
PDOStatement::execute returns false when the query results in an error, it doesn't throw an exception. You should probably write some code to check that return value if you want to do something with the error.