Setting Up Couchbase as a PHP Session Handler with igbinary Encoding

Hello Couchbase community,

I’d like to share my experience in setting up Couchbase as a session handler in PHP and using igbinary for efficient encoding and decoding. I’ve encountered a few challenges and would appreciate your insights and solutions.

I’ve been working on configuring Couchbase as a session handler for PHP applications. Here’s an overview of the steps I’ve taken:

  1. Couchbase Integration: I’ve successfully integrated Couchbase into my PHP environment using the official Couchbase SDK.
  2. Custom Session Handler: I’ve created a custom session handler class, CouchbaseSessionHandler, which implements the SessionHandlerInterface. This class manages session-related operations.
  3. PHP Configuration: I’ve configured the PHP session.save_handler to use “memcached”, set the session.save_path to specify the Couchbase server’s address and port (e.g., “couchbase://localhost:11211”).
  4. In my custom session handler, I handle session initiation, termination, read, write, and session data destruction.

All the code works as in supposed to but when i use and set the session.serialize_handler=igbinary i also need to set the couchbase transcoder to accept igbinary and here comes the problem:

I try like this:

$bucket->setTranscoder('igbinary_serialize', 'igbinary_unserialize');

“Call to a member function get() on bool” Error

<?php

require_once '/path_to/autoload.php';

class myCouchbase {
	var $classname = "myCouchbase";
    private $cluster = null;
    private $Cas = null;
    private \Couchbase\Exception\CouchbaseException $Exception;


	public function __construct($ConnectionString = "", $user = "", $password = "") {
		if ($ConnectionString && $user && $password) {
			$options = new \Couchbase\ClusterOptions();
			$options->credentials($user, $password);
			$cluster = new \Couchbase\Cluster($ConnectionString, $options);
			$this->cluster = $cluster; 
		}
	}


	public function getCollection($bucket_label, $scope_label, $collection_label){
		try {
			if ($this->cluster) {
				$bucket = $this->cluster->bucket($bucket_label);
				$bucket->setTranscoder('igbinary_serialize', 'igbinary_unserialize');
				if($bucket) $scope = $bucket->scope($scope_label);
				if($scope) $collection = $scope->collection($collection_label);
				return $collection;
			}
		} 	
		catch (\Couchbase\Exception\CouchbaseException $ex) {
				$this->Exception = $ex;
				return false;
			}		
	}


	public function getDocument($bucket_label, $scope_label, $collection_label, $key) {
		$collection = $this->getCollection($bucket_label, $scope_label, $collection_label, $key);
		try {	
			$result = $collection->get($key);
			$this->Cas = $result->cas();
			return $result->content();

		}	
		catch (\Couchbase\Exception\DocumentNotFoundException $ex) {
			$this->Cas = null;
			return false;
		}
		catch (\Couchbase\Exception\CouchbaseException $ex) {
			$this->Exception = $ex;
			return false;
		}
	}


	public function erase($bucket_label, $scope_label, $collection_label, $key) {
		$collection = $this->getCollection($bucket_label, $scope_label, $collection_label, $key);
		try {
			$collection->remove($key);
			return true;
		}
		catch (\Couchbase\Exception\CouchbaseException $ex) {
			if ($ex->getCode() == "COUCHBASE_KEY_ENOENT") {
				$this->Cas = null;
				return true;
			} else {
				$this->Exception = $ex;
				return false;
			}
		}
	}


	public function insert($bucket_label, $scope_label, $collection_label, $key, $value, $expiry = 0) {
		$collection = $this->getCollection($bucket_label, $scope_label, $collection_label, $key);
		try {
			$options = null;
			if ($expiry || $this->Cas) {
				$options = new \Couchbase\InsertOptions();
				if ($expiry) $options->expiry($expiry);
			}
			$result = $collection->insert($key, $value, $options ? $options : null);
			$this->Cas = $result->cas();
			return true;
		} 
		catch (\Couchbase\Exception\CouchbaseException $ex) {
			$this->Exception = $ex;
			return false;
		}
	}


	public function upsert($bucket_label, $scope_label, $collection_label, $key, $value, $expiry = 0) {
		$collection = $this->getCollection($bucket_label, $scope_label, $collection_label, $key);
		try {
			$options = null;
			if ($expiry || $this->Cas) {
				$options = new \Couchbase\UpsertOptions();
				if ($expiry) $options->expiry($expiry);
			}

			$result = $collection->upsert($key, $value, $options ? $options : null);
			return $result->cas();
		} 
		catch (\Couchbase\Exception\CouchbaseException $ex) {
			$this->Exception = $ex;
			return false;
		}
	}
	
	
	public function get_cluster(){
		return $this->cluster;
	}

}

?>

The bizarre thing is that when I don’t use igbinary and serialize the session with the default PHP serialization, everything works as expected.

Note:
I forgot to say if I didn’t have this line:

$bucket->setTranscoder('igbinary_serialize', 'igbinary_unserialize');
Fatal error: Uncaught JsonException: Malformed UTF-8 characters, possibly incorrectly encoded in Couchbase/JsonTranscoder.php on line 77JsonException: Malformed UTF-8 characters, possibly incorrectly encoded in Couchbase/JsonTranscoder.php on line 77

Does that error occur on the setTranscoder() call?

In my custom session handler class, CouchbaseSessionHandler, which utilizes the myCouchbase code as described above, I’ve try to configured the Couchbase transcoder to handle “igbinary” serialization and deserialization with the following line:

$bucket->setTranscoder('igbinary_serialize', 'igbinary_unserialize');

However, the issue arises after executing the code in the session.php file:

  1. I create a CouchbaseSessionHandler object.
  2. I assign it using session_set_save_handler($couchbaseSessionHandleObject).
  3. I define $_SESSION['data'].
  4. I invoke session_start().

It’s at this point that I encounter the following error:

"Call to a member function get() on bool"

To clarify, yes indeed, the error occur during the execution of the setTranscoder() call. It happens when I attempt to use the Couchbase session handler for reading session data from Couchbase after setting up the “igbinary” transcoder.

I think the issue seems to be related to the integration of the “igbinary” transcoder with Couchbase in the context of session handling.

I’m confused.

  1. I invoke session_start().

It’s at this point that I encounter the following error:

"Call to a member function get() on bool"

So it happens on the call to session_start() ?

To clarify, yes indeed, the error occur during the execution of the setTranscoder() call.

So it happens on the call to setTranscoder() ?

It happens when I attempt to use the Couchbase session handler for reading session data from Couchbase after setting up the “igbinary” transcoder.

So it happens after setting up the transcoder, during the reading of session data?

I apologize for any confusion.

The error, "Call to a member function get() on bool," occurs when calling setTranscoder().

Without the line of setTranscoder(), I get this error:
"Fatal error: Uncaught JsonException: Malformed UTF-8 characters, possibly incorrectly encoded in Couchbase/JsonTranscoder.php on line 77."

What version of the Couchbase PHP SDK do you have? It seems that setTranscoder() is not supported after 4.0.

With PHP SDK 4.1.6, I’m able to use ignbinary with this Transcoder. flags are ignored.


<?php
declare(strict_types=1);
namespace Couchbase;
class IgbinaryTranscoder implements Transcoder
{
    private static ?IgbinaryTranscoder $instance;

    public static function getInstance(): Transcoder
    {
        if (!isset(self::$instance)) {
            self::$instance = new IgbinaryTranscoder();
        }
        return self::$instance;
    }

    /**
     *
     * @param mixed $value document
     *
     * @return array tuple of encoded value with flags for network layer
     */
    public function encode($value): array
    {
        return [
            igbinary_serialize($value), 0
        ];
    }

    /**
     *
     * @param string $bytes encoded data
     * @param int $flags flags from network layer, that describes format of the encoded data
     *
     * @return mixed decoded document
     */
    public function decode(string $bytes, int $flags)
    {
        return igbinary_unserialize($bytes);
    }
}

calling code is

	$tx = IgbinaryTranscoder::getInstance();

	$uoptions = new UpsertOptions();
	$uoptions->transcoder($tx);
        $ret = $collection->upsert($key, '{"test":"test"}', $uoptions);

	$options = new GetOptions();
	$options->transcoder($tx);
        $ret = $collection->get($key, $options);
        print($ret->content());
1 Like

Thank you for your response, and I apologize for the delayed response on my part.

I am currently utilizing Couchbase PHP SDK version 4.1.5.

I can confirm that the provided code worked perfectly with the test data '{"test":"test"}', exactly as you specified.

However, when I attempted to use my session data test, which is a JSON-formatted file in UTF-8 encoding and has a size of approximately 1.76 MB, I encountered an error.

This error occurred during the code execution, specifically when the upsert function was called within the CouchbaseSessionHandler class.

I have included the updated code below:

<?php
declare(strict_types=1);

class IgbinaryTranscoder implements \Couchbase\Transcoder
{
    private static ?IgbinaryTranscoder $instance;

    public static function getInstance(): \Couchbase\Transcoder
    {
        if (!isset(self::$instance)) {
            self::$instance = new IgbinaryTranscoder();
        }
        return self::$instance;
    }

    /**
     *
     * @param mixed $value document
     *
     * @return array tuple of encoded value with flags for network layer
     */
    public function encode($value): array
    {
        return [
            igbinary_serialize($value), 0
        ];
    }

    /**
     *
     * @param string $bytes encoded data
     * @param int $flags flags from network layer, that describes format of the encoded data
     *
     * @return mixed decoded document
     */
    public function decode(string $bytes, int $flags)
    {
        return igbinary_unserialize($bytes);
    }
}

?>
<?php

require_once 'path_to/autoload.php';
include 'IgbinaryTranscoder.cla';

class myCouchbase {
	var $classname = "myCouchbase";
    private $cluster = null;
    private $Cas = null;
    private \Couchbase\Exception\CouchbaseException $Exception;
	private $tx;


	public function __construct($ConnectionString = "", $user = "", $password = "") {
		if ($ConnectionString && $user && $password) {
			$options = new \Couchbase\ClusterOptions();
			$options->credentials($user, $password);
			$cluster = new \Couchbase\Cluster($ConnectionString, $options);
			$this->cluster = $cluster; 
			$this->tx = IgbinaryTranscoder::getInstance();
		}
	}


	public function getCollection($bucket_label, $scope_label, $collection_label){
		try {
			if ($this->cluster) {
				$bucket = $this->cluster->bucket($bucket_label);
				if($bucket) $scope = $bucket->scope($scope_label);
				if($scope) $collection = $scope->collection($collection_label);
				return $collection;
			}
		} 	
		catch (\Couchbase\Exception\CouchbaseException $ex) {
				$this->Exception = $ex;
				return false;
			}		
	}


	public function getDocument($bucket_label, $scope_label, $collection_label, $key) {
		$collection = $this->getCollection($bucket_label, $scope_label, $collection_label, $key);
		try {	
			$options = new \Couchbase\GetOptions();
			$options->transcoder($this->tx);
			$result = $collection->get($key, $options);
			$this->Cas = $result->cas();
			return $result->content();

		}	
		catch (\Couchbase\Exception\DocumentNotFoundException $ex) {
			$this->Cas = null;
			return false;
		}
		catch (\Couchbase\Exception\CouchbaseException $ex) {
			$this->Exception = $ex;
			return false;
		}
	}


	public function erase($bucket_label, $scope_label, $collection_label, $key) {
		$collection = $this->getCollection($bucket_label, $scope_label, $collection_label, $key);
		try {
			$collection->remove($key);
			return true;
		}
		catch (\Couchbase\Exception\CouchbaseException $ex) {
			if ($ex->getCode() == "COUCHBASE_KEY_ENOENT") {
				$this->Cas = null;
				return true;
			} else {
				$this->Exception = $ex;
				return false;
			}
		}
	}


	public function insert($bucket_label, $scope_label, $collection_label, $key, $value, $expiry = 0) {
		$collection = $this->getCollection($bucket_label, $scope_label, $collection_label, $key);
		try {
			$options = null;
			if ($expiry || $this->Cas) {
				$options = new \Couchbase\InsertOptions();
				if ($expiry) $options->expiry($expiry);
			}
			$result = $collection->insert($key, $value, $options ? $options : null);
			$this->Cas = $result->cas();
			return true;
		} 
		catch (\Couchbase\Exception\CouchbaseException $ex) {
			$this->Exception = $ex;
			return false;
		}
	}


	public function upsert($bucket_label, $scope_label, $collection_label, $key, $value, $expiry = 0) {
		$collection = $this->getCollection($bucket_label, $scope_label, $collection_label, $key);
		try {
			$options = null;
			if ($expiry || $this->Cas) {
				$options = new \Couchbase\UpsertOptions();
				if ($expiry) $options->expiry($expiry);
				$options->transcoder($this->tx);
			}

			$result = $collection->upsert($key, $value, $options ? $options : null);
			return $result->cas();
		} 
		catch (\Couchbase\Exception\CouchbaseException $ex) {
			$this->Exception = $ex;
			return false;
		}
	}
	
	
	public function get_cluster(){
		return $this->cluster;
	}

}

?>

To assist you better in resolving this issue, I will now provide the error:

Fatal error: Uncaught JsonException: Malformed UTF-8 characters, possibly incorrectly encoded in Couchbase/JsonTranscoder.php on line 77

JsonException: Malformed UTF-8 characters, possibly incorrectly encoded in Couchbase/JsonTranscoder.php on line 77

Thank you for your assistance.

Note:

I use this code to put start the session:

<?php

ini_set('session.serialize_handler', 'igbinary');

require_once 'CouchbaseSessionHandler.cla';

$jsonFilePath = 'BigSession.json';

$jsonData = file_get_contents($jsonFilePath);

$data = json_decode($jsonData);

$sessionHandler = new CouchbaseSessionHandler();

session_set_save_handler($sessionHandler);

session_start();

$_SESSION['data'] = $data;

?>

The error message comes from Couchbase/JsonTranscoder, which would only be called if $options was null or $options->transcoder was not set. You’ll need to set $options->transcoder() regardless.

1 Like

It works perfectly now. I really appreciate your time and assistance. Have a fantastic day!

I have a question,
(Sorry for asking again)

When I use test data and only the upsert and get functions, the encode and decode work perfectly, as you told me.

But when it comes to the session, (i will post the code below) the decode does not work. (I used print_r() in the decode function of the igbinary class, and it prints.)

Do you have any clues Mr.@mreiche?

<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('session.serialize_handler', 'igbinary');
require_once 'CouchbaseSessionHandler.cla';
$jsonFilePath = 'BigSession.json';
$jsonData = file_get_contents($jsonFilePath);
$data = json_decode($jsonData);
$sessionHandler = new CouchbaseSessionHandler();
session_set_save_handler($sessionHandler);
$ssid = session_id();
session_start();
$_SESSION['data'] = $data;
session_write_close();
$session_data = $sessionHandler->read($ssid);
print_r(session_data); //Still Serialized data???
?>

If you expect session_data to be deserialized from the read() function, you’ll need to debug the read() function.

Also - isn’t the $ sign missing in print_r(session_data)? So the code you are showing is not the code you are running.

Also - couchbase has compression. Why not just rely on that instead of igbinary which is causing you no end of problems?

Sorry, I wasn’t clear and thanks for your time Mr.@mreiche.

The read() function only call the getDocument() function, which is the same function in the code above:

...
 public function read($id) : string {
        $key = $id;
        $return = $this->CB->getDocument($this->_bucketLabel, $this->_scopeLabel, $this->_collectionLabel, $key);
        return $return ?: '';
    }
...

I’m already using compression in passive mode; I want to have the fattest way to handle the sessions, which is why I use igbinary.

Sorry for the $ sign, the code I’m using does the same, but inside a loop, when I erased the for statement, I also erased the print_r line, so when I rewrote, I forgot the $…

Ok, you’ll need to debug your getDocument() function. Btw - when you are debugging, it’s not a good idea to catch and ignore exceptions. Also “doesn’t work” is not a good description.

Your insert() has the same problem that was in upsert().

And any document you have inserted/upserted like that (with the default JsonTranscoder) cannot be get()-ed with the igbinary transcoder.

Thanks a lot Mr.@mreiche I´m getting close :smile::

So as you say, if the code don’t have a transcode option set, it will get an JsonTranscoder error because igbinary is non-JSON data

But for my case, the session.serialize_handler=igbinary serializes and unserialize the session for me (or is supposed to)… So i don’t need the couchbase to encode and decode; I only need the couchbase to accept the igbinary when writing and get the igbinary content when reading from the db.

So because the session.serialize_handler=igbinary serializes the data for me and the read() method in the CouchbaseSessionHandler only accepts serialized data, I need to do the IgbinaryTranscoder like this:

<?php
declare(strict_types=1);
use \Couchbase\Transcoder;
class IgbinaryTranscoder implements Transcoder
{
    private static ?IgbinaryTranscoder $instance;

    public static function getInstance(): Transcoder
    {
        if (!isset(self::$instance)) {
            self::$instance = new IgbinaryTranscoder();
        }
        return self::$instance;
    }

    /**
     *
     * @param mixed $value document
     *
     * @return array tuple of encoded value with flags for network layer
     */
    public function encode($value): array
    {   
        return [$value, 0];
    }

    /**
     *
     * @param string $bytes encoded data
     * @param int $flags flags from network layer, that describes format of the encoded data
     *
     * @return mixed decoded document
     */
    public function decode(string $bytes, int $flags)
    {
        return $bytes;
    }
}

But when I print the read data, it’s still serialized (the session.serialize_handler is supposed to serialize and unserialize, but it only serializes in the begging after the session_start()…)

<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('session.serialize_handler', 'igbinary');
require_once 'CouchbaseSessionHandler.cla';
$jsonFilePath = 'BigSession.json';
$jsonData = file_get_contents($jsonFilePath);
$data = json_decode($jsonData);

//Start Counting
$start_time = microtime(true);
for ($i = 1; $i <= 1; $i++) {  
    $sessionHandler = new CouchbaseSessionHandler();
    session_set_save_handler($sessionHandler);
    session_start();
    $_SESSION['data'] = $data;
    session_write_close();
    $ssid = session_id();
    $session_data = $sessionHandler->read($ssid);
    print_r($session_data);
}
//End Counting
$end_time = microtime(true);
$execution_time = ($end_time - $start_time) * 1000;

echo "Script execution time: " . $execution_time . " milliseconds";
?>

when I print the read data, it’s still serialized (the session.serialize_handler is supposed to serialize and unserialize".

So where does session.serialize_handler deserialize $session_data between

$session_data = $sessionHandler->read($ssid);

- and -

print_r($session_data);

Its a built in functionality of php for sessions:

it’s a functionality that you are not using between the read and the print. You’re just printing out the serialized data. It’s going to be serialized. If session.serialize_handler is going to deserialized, doesn’t the data need to be ran through that somewhere?

btw - you don’t need to create a no-op transcoder. Just use Transcoders and Non-JSON Documents | Couchbase Docs

If the results from your read() function are not correct, you’ll need to fix that in your read() function.