Watching this online tutorial about MYSQL's session handler and got really confused about this part:
table_XXX == Table XXX;
col_XXX == Column XXX;
sid == Session id
Read method:
public function read($session_id)
{
$this->db->exec('SET TRANSACTION ISOLATION LEVEL READ COMMITTED');
$this->db->beginTransaction();
/**
* the data is selected and no other ppl can interfere
* the writing process until COMMIT is reached
*/
$sql = "SELECT $this->col_expiry, $this->col_data
FROM $this->table_sess
WHERE $this->col_sid = :sid FOR UPDATE";
$selectStmt = $this->db->prepare($sql);
$selectStmt->bindParam(':sid', $session_id);
$selectStmt->execute();
$results = $selectStmt->fetch(\PDO::FETCH_ASSOC);
if ($results) {
if ($results[$this->col_expiry] < time()) {
// return empty if data out of date
return '';
}
return $results[$this->col_data];
}
return $this->initializeRecord($selectStmt);
}
Protected method:
protected function initializeRecord(\PDOStatement $selectStmt)
{
try {
$sql = "INSERT INTO $this->table_sess
($this->col_sid, $this->col_expiry, $this->col_data)
VALUES (:sid, :expiry, :data)";
$insertStmt = $this->db->prepare($sql);
$insertStmt->bindParam(':sid', $session_id);
$insertStmt->bindParam(':expiry', $this->expiry); // expiry is defined
$insertStmt->bindValue(':data', '');
$insertStmt->execute();
return '';
} catch(\PDOException $e) {
$this->db->rollBack();
throw $e;
}
}
Write method:
public function write($session_id, $data)
{
try {
$sql = "INSERT INTO $this->table_sess ($this->col_sid,
$this->col_expiry, $this->col_data)
VALUES (:sid, :expiry, :data)
ON DUPLICATE KEY UPDATE
$this->col_expiry = :expiry,
$this->col_data = :data";
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':expiry', $this->expiry, \PDO::PARAM_INT);
$stmt->bindParam(':data', $data);
$stmt->bindParam(':sid', $session_id);
$stmt->execute();
return true;
} catch (\PDOException $e) {
if ($this->db->inTransaction()) {
$this->db->rollback();
}
throw $e;
}
}
In 'Protected method', line 8, there is a $session_id, and clearly no $session_id is passed to the protected method, so bindParam() for that line simply binded nothing?
So initializeRecord() simply initiated a row that has expiry time but nothing else? And then the sid and data is inserted after write method is called?
This is doing a lot of string-construction trickery with WHERE $this->col_sid = :sid and so forth, as it creates SQL statements.
You might try echoing or dumping those SQL statements to see what they contain right before you run ->execute() on them. That will help you troubleshoot.
It's pretty clear your protected method is missing $session_id. Is it possible there's a value for $this->sid you could use there?
Related
OK, I'm fully expecting someone to come along and tell me I missed something obvious, but I've spent days trying to figure out what's wrong, and I at this point, if someone can point out a stupid mistake, I'll just be glad to solve the problem. I'm just starting to figure out PDO, so I'm guessing the problem is there, but I really don't know.
This is mostly working. It opens, closes, reads, and does garbage collection. It will write, but only the id, not the data. Destroy is not working either, which seems to be related. If I put echo into the functions, the variables are being passed. The queries work when I try them directly. What seems to be happening is that the binding isn't working. I've tried both bindParam and bindValue. I've tried so many other things I can't even list them all. So anyway, here's the class:
class Sessions {
private $id;
private $data;
protected $pdo;
private $maxTime;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
// Set handler to overide SESSION
#session_set_save_handler(
array($this, "openSession"),
array($this, "closeSession"),
array($this, "readSession"),
array($this, "writeSession"),
array($this, "destroySession"),
array($this, "gcSession")
);
session_start();
}
public function openSession()
{
// If successful
if ($this->pdo) {
return true;
} else {
return false;
}
}
public function closeSession() {
// Close the database connection
if ($pdo = NULL) {
#session_write_close();
return true;
} else {
return false;
}
}
public function readSession($id) {
if (isset($id)) {
// Set query
$q = 'SELECT data FROM sessions WHERE id = :id';
$stmt = $this->pdo -> prepare($q);
// Bind the Id
$stmt -> bindValue(':id', $id);
$stmt -> execute();
$r = $stmt -> fetch();
// Return the data
if (!empty($r['data'])) {
return $r['data'];
} else {
// Return an empty string
return '';
}
} else {
return '';
}
}
// when session started and updated
public function writeSession($id, $data) {
// Set query
$q = "INSERT INTO sessions
(id, `data`, startTime)
VALUES (:id, :data, NOW())
ON DUPLICATE KEY UPDATE
`data` = :data2,
startTime = NOW()";
$stmt = $this->pdo->prepare($q);
// Bind data
$stmt->bindParam(':id', $id);
$stmt->bindParam(':data', $data);
$stmt->bindParam(':data2', $data);
$stmt->execute();
if ($stmt->rowCount()) {
return TRUE;
} else {
return FALSE;
}
}// when session started and updated
public function writeSession($id, $data) {
// Set query
$q = "INSERT INTO sessions (id, `data`, startTime) VALUES (:id, :data, NOW()) ON DUPLICATE KEY UPDATE `data` = :data2, startTime = NOW()";
$stmt = $this->pdo -> prepare($q);
// Bind data
$stmt->bindParam(':id', $id);
$stmt->bindParam(':data', $data);
$stmt->bindParam(':data2', $data);
$stmt->execute();
if ($stmt->rowCount()) {
return TRUE;
} else {
return FALSE;
}
}
public function destroySession($id) {
echo $id;
$q = "DELETE FROM `sessions` WHERE `id`= :id";
$stmt = $this->pdo -> prepare($q);
$stmt->execute(array(':id' => $id));
// Attempt execution
if ($stmt === true) {
echo " worked ";
return TRUE;
} else {
echo " no ";
return FALSE;
}
}
public function gcSession($maxTime) {
$q="DELETE FROM sessions WHERE (NOW()-startTime > $maxTime)";
$stmt = $this->pdo -> prepare($q);
if ($stmt->execute()) {
return TRUE;
} else {
return FALSE;
}
}
}
The function is being called this way:
// Store the user in the session and redirect:
$startSessions = new Sessions($pdo);
$_SESSION['data'] = 'IDnotOK';
$startSessions -> writeSession(session_id(), 'IDallOK');
$startSessions -> gcSession(1800);
The 'IDnotOK' is being sent to the database (sort of - it's 'data|s:7:"IDnotOK";'), instead of 'IDallOK'. If the $_SESSION['data'] isn't there, nothing is sent at all. The id is passed just fine. The timestamp also updates just fine. When I call:
$startSessions -> destroySession(session_id());
it doesn't matter how I try to pass the id, it will echo from within the function, but won't do anything to the database entry. Garbage collections works (probably because it only relies on the time and doesn't care about the id).
I wish I could list everything I've tried, but I've lost track. If there are any other questions that will help track down the problem, please ask. Thank You!
I am using a few functions inside another function that updates certain things, deletes some and inserts, now my problem, if 2 above are successful or 1 and the rest aren't it could cause catastrophic outcomes. So is it possible to use 1 transaction for all 3 functions... for example:
public static function do_stuff()
{
//run sql in function
SELF::function_sql_one_insert();
SELF::function_sql_two_update();
SELF::function_sql_three_delete();
}
Like so:
public static function test()
{
SELF::function_sql_one_insert();
SELF::function_sql_two_update();
SELF::function_sql_three_delete();
}
public static function function_sql_one_insert()
{
//sql to run
$sql = "INSERT INTO table
(
fake_row_one,
fake_row_two
)
VALUES
(
?,
?
)";
//run sql
$fake_insert = $database->prepare($sql);
$fake_insert->execute("yeah", "okay");
}
public static function function_sql_two_update()
{
//sql to run
$sql = "UPDATE table
SET fake_row_one = ?
WHERE fake_row_two = ?";
//run sql
$fake_update = $database->prepare($sql);
$fake_update->execute("blahblah", "okay");
}
public static function function_sql_three_delete()
{
//sql to run
$sql = "DELETE FROM TABLE
WHERE fake_row_two = ?";
//run sql
$fake_delete = $database->prepare($sql);
$fake_delete->execute("okay");
}
What I am trying to acomplish is if one fails revert all of them back. Is this possible? If not what can I do instead, if so, is there any cons to this?
Php functions has absolutely nothing to do with database transactions. It's just irrelevant matters.
A database transaction is bound to database connection only. Thetefore, as long as all your functions use the same connection, there is no problem to run all three in a transaction.
You should use database transactions for this.
Essentially,
you start a transaction,
you do your SQL queries
if it fails somewhere, you do a rollback and otherwise you do a commit
But there are a few gotcha's such as certain sql statements that commit on their own, so read all the official docs for the database you're using.
More info: http://php.net/manual/en/pdo.begintransaction.php
But remember that not all databases support transactions to start with.
E.g. MyISAM tables in MySQL do not support transactions.
You might want to convert those tables to InnoDB see here e.g.: How to convert all tables from MyISAM into InnoDB?
I use this way:
class Teste {
public $mysqli;
public $erro = array();
public function __construct(){
$this->mysqli = new \mysqli(DB_HOST,DB_USERNAME,DB_PASSWORD,DATABASE);
$this->mysqli->set_charset("utf8");
}
public function start_trans(){
$this->mysqli->autocommit(false);
}
public function end_trans(){
if(count($this->erro) == 0){
$this->mysqli->commit();
echo "success";
} else {
$this->mysqli->rollback();
echo "error";
}
}
public function example1(){
$stmt = $this->mysqli->query("insert into veiculos (placa, modelo) values (1,1)");
if(!$stmt){
$this->erro[] = "Erro #143309082017 <code>" . $this->mysqli->error . "</code>";
}
return (count($this->erro) < 1)? true : false;
}
public function example2(){
$stmt = $this->mysqli->query("insert into veiculos (placa, modelo) values (2,2)");
if(!$stmt){
$this->erro[] = "Erro #143309082017 <code>" . $this->mysqli->error . "</code>";
}
return (count($this->erro) < 1)? true : false;
}
public function example3(){
$this->mysqli->autocommit(false);
$stmt = $this->mysqli->query("insert into veiculos (placa, modelo) values (3,3)");
if(!$stmt){
$this->erro[] = "Erro #143309082017 <code>" . $this->mysqli->error . "</code>";
}
return (count($this->erro) < 1)? true : false;
}
}
$action = new Teste;
$action->start_trans();
$action->example1();
$action->example2();
$action->example3();
$action->end_trans();
Maybe you can try something like this:
$working = true;
try
{
SELF::function_sql_one_insert();
} catch (Exception $e)
{
if($e != "")
$working = false;
}
Try this for all functions. If $working is true, you can execute all commands.
I've searched on stackoverflow and other sources but I cant seem to find the issue that is preventing my PHP script from working.
Look at the echo_sql. It produces a healthy update statement which when run updates the database with no problem. Here is a sample:
update waste set waste_name=1 where id =82;
However, when the script is run, it does not apply changes to the database. Here is the script:
if ($_SERVER['REQUEST_METHOD'] == "POST") {
try {
$waste_id = $_POST['waste_id'];
$sql = new db;
$sql->beginTransaction();
$waste_name = $_POST['waste_name'];
$sql->query("update waste set waste_name=:waste_name where id =:waste_id;");
$echo_sql = "update waste set waste_name=$waste_name where id =$waste_id;";
echo $echo_sql;
$sql->bind(':waste_name', $waste_name);
$sql->execute();
$sql->endTransaction();
} catch (Exception $e) {
$sql->rollBack();
echo "Failed: " . $e->getMessage();
}
}
Additional details:
errorCode() = 00000
DB Class:
class db
{
private $stmt;
private $dbc;
public function __construct()
{
$u = "root";
$p = "";
try {
$this->dbc = new PDO('mysql:host=127.0.0.1;dbname=wimsdb', $u, $p);
$this->dbc->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
$e->getMessage();
}
}
public function bind($param, $value, $type = NULL)
{
$this->stmt->bindParam($param, $value, $type);
}
public function beginTransaction()
{
return $this->dbc->beginTransaction();
}
public function rollBack()
{
return $this->dbc->rollBack();
}
public function endTransaction()
{
return $this->dbc->commit();
}
public function cancelTransaction()
{
return $this->dbc->rollBack();
}
public function execute()
{
try {
return $this->stmt->execute();
} catch (PDOException $e) {
return $e->errorInfo;
}
}
public function errorCode()
{
return $this->stmt->errorCode();
}
public function query($query)
{
$this->stmt = $this->dbc->prepare($query);
}
}
Please offer your suggestions on how this could be resolved.
You need to bind the :waste_id too:
$waste_id = $_POST['waste_id'];
$sql = new db;
$sql->beginTransaction();
$waste_name = $_POST['waste_name'];
$sql->query("update waste set waste_name=:waste_name where id =:waste_id;");
$sql->bind(':waste_name', $waste_name);
$sql->bind(':waste_id', $waste_id);
Any time you have an issue like this your error checking should return a meaningful message letting you know where the error is and likely what the error is. You should be able to check your error logs for details and/or output them to your screen during testing.
Add waste_id. To avoid missing parameters, I like putting the parameteers into the execute method. The bind method could be defined anywhere in the code so I had to look through your code and make sure waste_id binding wasn't defined somewhere else. When it's in the execute method, you can quickly see all parameters being defined there...it's also a tad more concise...but both have their uses.
if ($_SERVER['REQUEST_METHOD'] == "POST") {
try {
$waste_id = $_POST['waste_id'];
$sql = new db;
$sql->beginTransaction();
$waste_name = $_POST['waste_name'];
$sql->query("update waste set waste_name=:waste_name where id =:waste_id;");
$echo_sql = "update waste set waste_name=$waste_name where id =$waste_id;";
echo $echo_sql;
//just because I like this syntax for being concise and clear :)
$sql->execute(array(
'waste_id' => $waste_id,
'waste_name' => $waste_name
));
$sql->endTransaction();
} catch (Exception $e) {
$sql->rollBack();
echo "Failed: " . $e->getMessage();
}
This code section is used on another page where results are to be displayed.
<?php
require 'core/init.php'; //all classes are contained in here.
$general->logged_out_protect();
$search = $_POST['search'];
if ($users->user_exists($_POST['search']) == false) {
$errors[] = "Sorry that username doesn't exists";
} else
if ($users->user_exists($_POST['search']) == true) {
// i would like to display username which is in the user_exists if the above condition is met.
}
}
?>
//This is function user_exists in which i determine if username is in database then after i display the username.
public function user_exists($username) {
$query = $this->db->prepare("SELECT COUNT(`id`) FROM `users` WHERE `username`= ?");
$query->bindValue(1, $username);
try{
$query->execute();
$rows = $query->fetchColumn();
if($rows == 1){
return true;
}else{
return false;
}
} catch (PDOException $e){
die($e->getMessage());
}
}
You either use PDO and execute: http://php.net/manual/en/pdo.prepare.php
$sth->execute(array(':calories' => 150, ':colour' => 'red'));
or mysqli and bind param and then execute: http://php.net/manual/en/mysqli.prepare.php
if ($stmt = $mysqli->prepare("SELECT District FROM City WHERE Name=?")) {
/* bind parameters for markers */
$stmt->bind_param("s", $city);
/* execute query */
$stmt->execute();
note if you use prepare:
you don't need to escape your input string
you need to use ? as placeholder or :placeholder : http://php.net/manual/en/mysqli-stmt.bind-param.php
Query debugging:
to get mysqli errors use: or die(mysqli_error($db) after your execute or query call.
solution
change the order of your execute() and bind_param() (first bind_param() then execute())
your sql query should be: "SELECT * FROM users WHERE username like '%$?%'"
Several programming suggestions.
PHP is dynamic typed, so a variable or function result value, may return different variable types, stick to single type.
In some circumstances null could be used, instead of the predefined type, for example,
returning null when a search is not found, instead of an integer value.
For PHP, use comments to determine which data type a function receives or returns.
Before.
public function user_exists($username) {
After.
public /* bool */ function user_exists(/* string */ $username) {
It doesn't change your code logic, but, helps any programmer, either you or another person, to understand the logic of the function.
When using a "try" sentence in a function that returns a value (non void function), only use a single "try" sentence.
When using a "try" sentence in a function that returns a value (non void function), declare the local variables with empty values before the "try", and make all assignaments inside the "try":
Before.
public function user_exists($username) {
$query = $this->db->prepare("SELECT COUNT(`id`) FROM `users` WHERE `username`= ?");
$query->bindValue(1, $username);
try{
$query->execute();
$rows = $query->fetchColumn();
if($rows == 1){
return true;
}else{
return false;
}
} catch (PDOException $e){
die($e->getMessage());
}
}
After.
public function user_exists($username) {
$query = null;
try{
$query = $this->db->prepare("SELECT COUNT(`id`) FROM `users` WHERE `username`= ?");
$query->bindValue(1, $username);
$query->execute();
$rows = $query->fetchColumn();
if($rows == 1){
return true;
}else{
return false;
}
} catch (PDOException $e){
die($e->getMessage());
}
}
This allows you to read their values in the catch section when an exception is generated,
or to clean them either in the catch sections or finally sections.
Even that the PHP enviroment is garbaged collected as: Java, ".Net", and other programming enviroments, some good "House Cleaning" is welcome, and helps you have more control of your programming logic.
When using a "try" sentence in a function that returns a value (non void function), assign the exception result to a local variable, and transfer the die to a "finally" section.
Example:
public function user_exists($username) {
$query = null;
$ExceptionMsg = "";
$AnyException = false;
try{
$query = $this->db->prepare("SELECT COUNT(`id`) FROM `users` WHERE `username`= ?");
$query->bindValue(1, $username);
$query->execute();
$rows = $query->fetchColumn();
if($rows == 1){
return true;
}else{
return false;
}
} catch (PDOException $e){
$ExceptionMsg = $e->getMessage();
$AnyException = true;
} finally{
if ($AnyException)
die($ExceptionMsg);
}
}
Cheers.
I have an odd issue. The first time a visitor comes to the site and I set anything is the session, it doesn't stick. The second and all the following times I try to set something it sticks. After the initial try I can destroy the session and set something and it sticks. Its just the initial attempt to save something fails. I'm trying to save something to the session with $_SESSION['uid'] = $row["Id"];. I know the $row["Id"] is valid and holds data (I echoed it).
I am not using standard sessions. I am saving the session into a database. My session class is below. Is there anything I'm missing or doing wrong to explain this behavior?
Update:
Well I tested the session class on its own and it seems to be working :-/ But when I use it in my larger application _write never gets called, though __destruct does get called. Any idea why that may be?
<?php
include_once('db.php');
class PDOSession
{
protected $pdo;
protected $table = 'SessionData';
public function __construct()
{
// Get a database connection
$db = new PDOConnectionFactory();
$this->pdo = $db->getConnection(true);
// Start session
session_set_save_handler(array($this, '_open'),
array($this, '_close'),
array($this, '_read'),
array($this, '_write'),
array($this, '_destroy'),
array($this, '_gc'));
session_start();
}
public function __destruct()
{
session_write_close();
}
protected function fetchSession($id)
{
$stmt = $this->pdo->prepare('SELECT id, data FROM '.$this->table.' WHERE id = :id AND unixtime > :unixtime');
$stmt->execute(array(':id' => $id, ':unixtime' => (time() - (int)ini_get('session.gc_maxlifetime'))));
$sessions = $stmt->fetchAll();
return empty($sessions) ? false : $sessions[0];
}
public function _open($savePath, $sessionName)
{
return true;
}
public function _close()
{
return true;
}
public function _read($id)
{
$session = $this->fetchSession($id);
return ($session === false) ? false : $session['data'];
}
public function _write($id, $sessionData)
{
$session = $this->fetchSession($id);
if($session === false) {
$stmt = $this->pdo->prepare('INSERT INTO '.$this->table.' (id, data, unixtime) VALUES (:id, :data, :time)');
} else {
$stmt = $this->pdo->prepare('UPDATE '.$this->table.' SET data = :data, unixtime = :time WHERE id = :id');
}
$stmt->execute(array(
':id' => $id,
':data' => $sessionData,
':time' => time()
));
}
public function _destroy($id)
{
$stmt = $this->pdo->prepare('DELETE FROM '.$this->table.' WHERE id = :id');
$stmt->execute(array(':id' => $id));
}
public function _gc($maxlifetime)
{
$stmt = $this->pdo->prepare('DELETE FROM '.$this->table.' WHERE unixtime < :time');
$stmt->execute(array(':time' => (time() - (int) $maxlifetime)));
}
}
$newPDOSessionStartHere = new PDOSession();
I'm a bit of an idiot I guess. I was calling session_destroy() rather than session_unset() to clear things out at the top of my authentication script. The class works fine.
I think that you should start a session after you define your class. Not inside.