Link to home
Start Free TrialLog in
Avatar of Marco Gasi
Marco GasiFlag for Spain

asked on

Can't save session data into the database

Hi everybody.
I'm trying to save PHP sessions to the database using mysqli but I'm struggking with an error I don't understand.

I use some classes autoloaded by Composer and everything worked fine until I have added the new classes Session and SessionModel. But first let me introduce the base class for DB connection, DbModel. Here the constructor:

<?php

namespace Emgie\EmgieDB;

use Emgie\EmgieConfig\EmgieConfigurator;

require __DIR__ . '/../../vendor/autoload.php';

if (!class_exists('DB')) {

  class DbModel
  {
    private $config;
    protected static $conn = null;
    protected $tableName;

    public function __construct($tableName)
    {
      $this->tableName = $tableName;
      $ec = new EmgieConfigurator();
      $this->config = $ec->getConfig();
      $this->connect();
    }

    private function connect()
    {
      if (!is_null(self::$conn)) {
        // a connection has already been established
        return;
      }

      self::$conn = new \mysqli($this->config->database->dbHost, $this->config->database->dbUserName, $this->config->database->dbPassword, $this->config->database->dbName);

      if (self::$conn->connect_error) {
        die("Connection failed: " . self::$conn->connect_errno . ' ' . self::$conn->connect_error);
      }
      self::$conn->set_charset('utf8');
    }
  }
}

Open in new window


All my ***Model classes extend this base class and they didn't give me any issues until now.

In Session class I have this:
<?php

namespace Emgie\EmgieUtils;

require __DIR__ . '/../../vendor/autoload.php';
/*code found at https://github.com/dominicklee/PHP-MySQL-Sessions*/

class Session
{


  private $model;

  public function __construct()
  {
    $this->model = new SessionModel();

    // Set handler to overide SESSION
    session_set_save_handler(
      array($this, "_open"),
      array($this, "_close"),
      array($this, "_read"),
      array($this, "_write"),
      array($this, "_destroy"),
      array($this, "_gc")
    );
    register_shutdown_function('session_write_close');
    // Start the session
    session_start();
  }

  public function _open()
  {
    return $this->model->checkConnection();
  }

  public function _read($id)
  {
    return $this->model->getSessionData($id);
  }
}

Open in new window


And here the insterested code inSessionModel
<?php

namespace Emgie\EmgieUtils;

use Emgie\EmgieDB\DbModel;

require __DIR__ . '/../../vendor/autoload.php';

class SessionModel extends DbModel
{

  protected $db;

  public function __construct()
  {
    parent::__construct('sessions');
    $this->db = parent::$conn;
  }

  public function checkConnection()
  {
    if (isset($this->db)) {
      return true;
    }
    return false;
  }

  public function getSessionData($id)
  {
    $query = "SELECT data FROM sessions WHERE id = ?";
    $stmt =  $this->db->stmt_init(); //line 31
    if ($stmt->prepare($query)) {
      $stmt->bind_param("s", $id);
      $stmt->execute();
      $result = $stmt->get_result();
      $row = $result->fetch_assoc();
      $stmt->close();
      return isset($row['data']) ? $row['data'] : '';
    }
    return '';
  }
}

Open in new window


The open function gives an error; Uncaught Error: Failed to open session: user

Forcing a true result of checkConnection method I get this

Uncaught Error: Call to a member function stmt_init() on null in /***/admin/emgie/emgieUtils/SessionModel.php:31

It looks like $this->db is not set but with the same code it is in every other ***Model class I'm using.
ANy idea.
Thank you for any help
Avatar of David Favor
David Favor
Flag of United States of America image

This means either $this->db or $this == NULL.

Best to wrap your access of these objects + output an error about which one is NULL.

Based on which is NULL, you'll debug from there.
Avatar of Marco Gasi

ASKER

Hello David, thank you fro your help. Great suggestion! Now I know something more and I'm going to share it with you:

  1.  the model in Session class is correctly initiated (not null)
  2. $this->db is an object in Model constructor...
  3. but $this->db is null in Model checkConnection()!
I'm investigating using this new info (but I still find it really strange)
I modified the Session class code in order to run the queries from within the class itself instead of use another class and this way above errors don't show up. But I get other errors!
 
First the new Session code:
<?php

namespace Emgie\EmgieUtils;

require __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/../../ChromePhp.php';

use Emgie\EmgieDB\DbModel;

class Session
{


  private $model;
  private $db;

  public function __construct()
  {
    $database = new DbModel('sessions');
    
    /*I have added a getter to DbModel in order to access the static property $conn*/  
    $this->db = $database->getConnection();

    // Set handler to overide SESSION
    session_set_save_handler(
      array($this, "_open"),
      array($this, "_close"),
      array($this, "_read"),
      array($this, "_write"),
      array($this, "_destroy"),
      array($this, "_gc")
    );
    register_shutdown_function('session_write_close');
    // Start the session
    session_start();
  }

  public function _open()
  {
    if(is_null($this->db)){
      \ChromePhp::log('db in _open is null');
      return false;
    }
    \ChromePhp::log('db in _open is ok');
    return true;
  }

  public function _close()
  {
    $this->db = null;
  }

  public function _read($id)
  {
    $query = "SELECT data FROM sessions WHERE id = ?";

    $stmt =  $this->db->stmt_init();
    if ($stmt->prepare($query)) {
      $stmt->bind_param("s", $id);
      $stmt->execute();
      $result = $stmt->get_result();
      $row = $result->fetch_assoc();
      $stmt->close();
      \ChromePhp::log('$row[data]');
      \ChromePhp::log($row['data']);
      return $row['data'];
    }
    \ChromePhp::log('_read returns an empty string');
    return '';
    // return $this->model->getSessionData($id);
  }

  public function _write($id, $data)
  {
    $access = time();
    $query = "REPLACE INTO sessions (id, access, data) VALUES (:id, :access, :data)";
    if ($stmt = $this->db->prepare($query)) {
      $stmt->bind_param("sis", $id, $access, $data);
      return $stmt->execute();
    }
    return false;
  }

  public function _destroy($id)
  {
    $query = "DELETE FROM sessions WHERE id = :id";
    $stmt = $this->db->prepare($query);
    $stmt->bind_param("s", $id);
    return $stmt->execute();
  }

  public function _gc($max)
  {
    $old = time() - $max;
    $query = "DELETE FROM sessions WHERE access < :old";
    $stmt = $this->db->prepare($query);
    $stmt->bind_param("i", $old);
   return  $stmt->execute();
  }

}



Open in new window

And now the warnings are:
Warning:  session_start(): Session callback expects true/false return value in /var/www/vhosts/38999564.servicio-online.net/dev.bodegasferrera.es/admin/emgie/emgieUtils/Session.php on line 64

Warning:  session_start(): Failed to read session data: user (path: /opt/alt/php72/var/lib/php/session) in /var/www/vhosts/38999564.servicio-online.net/dev.bodegasferrera.es/admin/emgie/emgieUtils/Session.php on line 64

line 64 is the one in constructor when session_start() is called.

Any idea?
Okay, here there is the new Session class. I have dropped the external SessionModel and merged the db logic.

<?php

namespace Emgie\EmgieUtils;

require __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/../../ChromePhp.php';

class Session
{

  private $db;
  private $tableName;

  public function __construct($tableName)
  {
    $database = new DbModel($tableName);
    $this->tableName = $tableName;

    $this->db = $database->getConnection();

    session_set_save_handler(
      array($this, "_open"),
      array($this, "_close"),
      array($this, "_read"),
      array($this, "_write"),
      array($this, "_destroy"),
      array($this, "_gc")
    );
    register_shutdown_function('session_write_close');

    session_start();
  }

  public function _open()
  {
    if (is_null($this->db)) {
      \ChromePhp::log('db in _open is null');
      return false;
    }
    \ChromePhp::log('db in _open is ok');
    return true;
  }

  public function _close()
  {
    $this->db = null;
    return true;
  }

  public function _read($id)
  {
    $query = "SELECT data FROM $this->tableName WHERE id = ?";

    $stmt =  $this->db->stmt_init();
    if ($stmt->prepare($query)) {
      $stmt->bind_param("s", $id);
      $stmt->execute();
      $result = $stmt->get_result();
      $row = $result->fetch_assoc();
      $stmt->close();
      \ChromePhp::log('$row[data]');
      \ChromePhp::log($row['data']);
      if (is_null($row['data'])) {
        return '';
      }
      return $row['data'];
    }
    \ChromePhp::log('_read returns an empty string');
    return '';
  }

  public function _write($id, $data)
  {
    $access = time();
    $query = "REPLACE INTO $this->tableName VALUES (?, ?, ?)";
    $stmt =  $this->db->stmt_init();
    if ($stmt->prepare($query)) {
      $stmt->bind_param("sis", $id, $access, $data);
      return $stmt->execute();
    }
    return false;
  }

  public function _destroy($id)
  {
    $query = "DELETE FROM $this->tableName WHERE id = :id";
    $stmt =  $this->db->stmt_init();
    if ($stmt->prepare($query)) {
      $stmt->bind_param("s", $id);
      return $stmt->execute();
    }
  }

  public function _gc($max)
  {
    $old = time() - $max;
    $query = "DELETE FROM $this->tableName WHERE access < :old";
    $stmt =  $this->db->stmt_init();
    if ($stmt->prepare($query)) {
      $stmt->bind_param("i", $old);
      return  $stmt->execute();
    }
  }
}



Open in new window

This time it looks to work but not really. when I call session_regenerate_id() I get this error:
Uncaught Error: Failed to open session: user 
So the question is now: how to use session_regenerate_id()  function when using database to save sessions?

ASKER CERTIFIED SOLUTION
Avatar of Marco Gasi
Marco Gasi
Flag of Spain image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
You're welcome!