Couchbase Datasource

Standard

I ran into a problem where I needed to utilize a NoSQL database. Since CakePHP is more fit for relational style database (MYSQL) I had to create a custom datasource. The following datasource allows models to have basic functionality of Couchbase Server.

Example Usage:

$keyName = "Jackson";
$cache = $this->YourModel->Get(array($keyName));
if (!$cache) {
   $data = array('Firstname' => 'Jessie',
                 'Lastname' => 'Jackson');
   $this->YourModel->Assign(array($keyName), json_encode($data));
}

Setup / Configuration:

To use the couchbaseSource you must specify it in your model. Do so by adding the following.
YourModel.php

public $useDbConfig = ‘bucketCB';

You can also programmatically change datasouces / database on the fly. For example, sometimes I want to use my default MYSQL datasource. This can be accomplished like so:

if($useNOSQL) {
   $this->Model->useDbConfig = 'bucketCB';
}else {
   $this->Model->useDbConfig = 'default'; // MYSQL
}

To use be able to use this datasource it must be added to cakePHP list of available datasources.
Database.php

public $bucketCB = array(
    'datasource' => 'CouchbaseSource',
    'username' => ‘bucketUsername,
    'password' => ‘bucketPassword’,
    'bucket' => ‘bucketName,
    'prefix' => ‘p’, // A _ is automatically prepended
    'expiry' => '1814400', // 3 Weeks
    'autoConnect' => true,
    'database' => NULL,
    'persistent' => false
);

app/Model/Datasource/CouchbaseSource.php

/**
 * Couchbase Datasource class
 * @Author Brandon Klimek
 * 
 */
class CouchbaseSource extends DataSource {

    /**
     * Description of datasource
     *
     * @var string
     */
    public $description = 'Couchbase DataSource';

    /**
     * Holds the object for the connected database
     *
     * @var object
     */
    public $conObject = NULL;

    /**
     * Holds the configuration settings that are passed in
     *
     * @var array
     */
    public $config = NULL;

    /**
     * The prefix of the couchbase keys
     *
     * @var string
     */
    public $prefix = NULL;

    /**
     * CouchDBSource Constructor
     *
     * @param array $config The configuration for the Datasource
     *
     * @return void
     * @link http://api.cakephp.org/class/data-source#method-DataSource__construct
     */
    public function __construct($config = array()) {

        // If no configuration is set we use the default
        $this->config = $config;

        // Setup the cache string that is used when building the string
        $this->prefix = (isset($this->config['prefix']) ? $this->config['prefix'] . "_" : "");

        if ($this->config['autoConnect']) {
            $this->connect();
        }
    }

    /**
     * Connect to the Datasource
     *
     * @return obj
     * @throws InternalErrorException
     */
    public function connect() {
        if ($this->conObject !== true) {
            try {
                $this->conObject = new Couchbase("127.0.0.1:8091", $this->config['username'], $this->config['password'], $this->config['bucket'], $this->config['persistent']);
            } catch (CouchbaseException $e) {
                throw new InternalErrorException(array('class' => $e->getMessage()));
            }
        }
        return $this->conObject;
    }

    /**
     * Handle queries to couchbase
     *
     * @param unknown_type $method
     * @param array() $param
     * @return array or false
     */
    public function query($method, $params) {

        // If not connected... reconnect!
        if ($this->conObject === NULL) {
            $this->connect();
        }

        $apiMethod = $this->__methodToClass($method);
        if (!method_exists($this, $apiMethod)) {
            throw new NotFoundException("Class '{$apiMethod}' was not found");
        } else {
            return call_user_func_array(array($this, $apiMethod), $params);
        }
    }

    /**
     * Translate method to className
     *
     * @param $method
     * @return string
     */
    private function __methodToClass($method) {
        return 'CB' . strtolower(Inflector::camelize($method));
    }

    /**
     * describe() tells the model your schema for ``Model::save()``.
     *
     * You may want a different schema for each model but still use a single
     * datasource. If this is your case then set a ``schema`` property on your
     * models and simply return ``$Model->schema`` here instead.
     */
    public function describe(&$Model) {
        return $this->description;
    }

    /////////////////////////////////////////////////
    // Query Methods
    /////////////////////////////////////////////////

    /**
     * Add a value with the specified key that does not already exist. Will fail if the key/value pair already exist.
     *
     * @return Contains the document ID or false if the operation failed
     */
    public function CBadd($key = NULL, $value = NULL, $expiry = NULL, $persisto = NULL, $replicateto = NULL) {
        return $this->conObject->add($key, $value, $expiry, $persisto, $replicateto);
    }

    /**
     * Append a value to an existing key
     *
     * @return scalar ( Binary object )
     */
    public function CBappend($key = NULL, $value = NULL, $expiry = NULL, $persisto = NULL, $replicateto = NULL) {
        return $this->conObject->append($key, $value, $expiry, $persisto, $replicateto);
    }

    /**
     * Compare and set a value providing the supplied CAS key matches
     *
     * @return scalar ( Binary object )
     */
    public function CBcas($casimoqie = NULL, $key = NULL, $value = NULL, $expiry = NULL) {
        return $this->conObject->cas($casimoqie, $key, $value, $expiry);
    }

    /**
     * Decrement the value of an existing numeric key. The Couchbase Server stores numbers as unsigned values. Therefore the lowest you can decrement is to zero.
     *
     * @return scalar ( Binary object )
     */
    public function CBdecrement($key = NULL, $offset = NULL) {
        return $this->conObject->decrement($key, $offset);
    }

    /**
     * Delete a key/value
     *
     * @return scalar ( Binary object )
     */
    public function CBdelete($key = NULL, $offset = NULL) {
        $this->conObject->delete($key, $offset);
    }

    /**
     * Wait until the durability of a document has been reached
     *
     * @return boolean ( Boolean (true/false) )
     */
    public function CBkeyDurability($key = NULL, $casunique = NULL) {
        return $this->conObject->keyDurability($key, $casunique);
    }

    /**
     * Wait until the durability of a document has been reached
     *
     * @return boolean ( Boolean (true/false) )
     */
    public function CBflush() {
        return $this->conObject->flush();
    }

    /**
     * Get a value and update the expiration time for a given key
     *
     * @return obj
     */
    public function CBgetAndTouch($key = NULL, $expiry = NULL) {
        return $this->conObject->getAndTouch($key, $expiry);
    }

    /**
     * Get a value and update the expiration time for a given key
     *
     * @return obj
     */
    public function CBgetAndTouchMulti($key = NULL, $expiry = NULL) {
        return $this->conObject->getAndTouchMult($key, $expiry);
    }

    /**
     * Fetch the next delayed result set document
     *
     * @return array ( Result list )
     */
    public function CBfetch($key = NULL, $keyn = NULL) {
        return $this->conObject->fetch($key, $keyn);
    }

    /**
     * Fetch all the delayed result set documents
     *
     * @return array ( Result list )
     */
    public function CBfetchAll($key = NULL, $keyn = NULL) {
        return $this->conObject->fetchAll($key, $keyn);
    }

    /**
     * Get one or more key values
     *
     * @return scalar ( Binary object )
     */
    public function CBget($key = NULL, $callback = NULL, $casunique = NULL) {

        if (is_array($key)) {
            $key = $this->buildCacheString($key);
        }
        return $this->conObject->get($key, $callback, $casunique);
    }

    /**
     * Get one or more key values
     *
     * @return boolean ( Boolean (true/false) )
     */
    public function CBgetDelayed($keyn = NULL, $with_cas = NULL, $callback = NULL) {
        return $this->conObject->getDelayed($keyn, $with_cas, $callback);
    }

    /**
     * Get one or more key values
     *
     * @return array ( Array of documents )
     */
    public function CBgetMulti($keycollection = NULL, $casarray = NULL) {
        return $this->conObject->getMulti($keycollection, $casarray);
    }

    /**
     * Returns the version of the client library
     *
     * @return scalar ( Binary object )
     */
    public function CBgetClientVersion() {
        return $this->conObject->getClientVersion();
    }

    /**
     * Get the value for a key, lock the key from changes
     *
     * @return (none)
     */
    public function CBgetAndLock($key = NULL, $casarray = NULL, $getlexpiry = NULL) {
        return $this->conObject->getAndLock($key, $casarray, $getlexpiry);
    }

    /**
     * Get the value for a key, lock the key from changes
     *
     * @return (none)
     */
    public function CBgetAndLockMulti($keycollection = NULL, $casarray = NULL, $getlexpiry = NULL) {
        return $this->conObject->getAndLockMulti($keycollection, $casarray, $getlexpiry);
    }

    /**
     * Returns the number of replicas for the configured bucket
     *
     * @return scalar ( Number of replicas )
     */
    public function CBgetNumReplicas() {
        return $this->conObject->getNumReplicas();
    }

    /**
     * Retrieve an option
     *
     * @return mixed ( Different possible types )
     */
    public function CBgetOption($option) {
        return $this->conObject->getOption($option);
    }

    /**
     * Returns the versions of all servers in the server pool
     *
     * @return array ( List of things )
     */
    public function CBgetVersion() {
        return $this->conObject->getVersion();
    }

    /**
     * Execute a view request
     *
     * @return (none)
     */
    public function CBview($ddocname = NULL, $viewname = NULL, $viewoptions = array()) {
        return $this->conObject->view($ddocname, $viewname, $viewoptions);
    }

    /**
     * Generate a view request, but do not execute the query
     *
     * @return (none)
     */
    public function CBviewGenQuery($ddocname = NULL, $viewname = NULL, $viewoptions = NULL) {
        return $this->conObject->viewGenQuery($ddocname, $viewname, $viewoptions);
    }

    /**
     * Increment the value of an existing numeric key. Couchbase Server stores numbers as unsigned numbers, therefore if 
     * you try to increment an existing negative number, it will cause an integer overflow and return a non-logical numeric 
     * result. If a key does not exist, this method will initialize it with the zero or a specified value.
     *
     * @return scalar ( Binary object )
     */
    public function CBincrement($key = NULL, $offset = NULL, $create = NULL, $expiry = NULL, $initial = NULL) {
        return $this->conObject->increment($key, $offset, $create, $expiry, $initial);
    }

    /**
     * Get the durability of a document
     *
     * @return boolean ( Boolean (true/false) )
     */
    public function CBobserve($key = NULL, $casunique = NULL, $observeddetails = NULL) {
        return $this->conObject->observe($key, $casunique, $observeddetails);
    }

    /**
     * Get the durability of a document
     *
     * @return boolean ( Boolean (true/false) )
     */
    public function CBobserveMulti($keycollection = NULL, $observeddetails = NULL) {
        return $this->conObject->observeMulti($keycollection, $observeddetails);
    }

    /**
     * Prepend a value to an existing key
     *
     * @return scalar ( Binary object )
     */
    public function CBprepend($key = NULL, $value = NULL, $expiry = NULL, $casunique = NULL, $persistto = NULL, $replicateto = NULL) {
        return $this->conObject->prepend($key, $value, $expiry, $casunique, $persistto, $replicateto);
    }

    /**
     * Update an existing key with a new value
     *
     * @return scalar ( Binary object )
     */
    public function CBreplace($key = NULL, $value = NULL, $expiry = NULL, $casunique = NULL, $persistto = NULL, $replicateto = NULL) {
        return $this->conObject->replace($key, $value, $expiry, $casunique, $persistto, $replicateto);
    }

    /**
     * Store a value using the specified key, whether the key already exists or not. Will overwrite a value if the given key/value already exists.
     *
     * @return scalar ( Binary object )
     */
    public function CBassign($key = NULL, $value = NULL, $expiry = NULL, $casunique = NULL, $persistto = NULL, $replicateto = NULL) {

        if (is_array($key)) {
            $key = $this->buildCacheString($key);
        }

        $time = (($expiry == NULL) ? $this->config['expiry'] : $expiry);

        try {
            $this->conObject->set($key, $value, $time, $casunique, $persistto, $replicateto);
        } catch (Exception $e) {
            return false;
        }
        return true;
    }

    /**
     * Set multiple key/value items at once
     *
     * @return scalar ( Binary object )
     */
    public function CBassignMulti($kvarray = NULL, $expiry = NULL) {
        return $this->conObject->setMulti($kvarray, $expiry);
    }

    /**
     * Specify an option
     *
     * @return boolean ( Boolean (true/false) )
     */
    public function CBsetOption($option = NULL, $mixed = NULL) {
        return $this->conObject->setOption($option, $mixed);
    }

    /**
     * Get the database statistics
     *
     * @return array ( List of things )
     */
    public function CBgetStats() {
        return $this->conObject->getStats();
    }

    /**
     * Update the expiry time of an item
     *
     * @return boolean ( Boolean (true/false) )
     */
    public function CBtouch($key = NULL, $expiry = NULL) {
        return $this->conObject->touch($key, $expiry);
    }

    /**
     * Change the expiration time for multiple documents
     *
     * @return boolean ( Boolean (true/false) )
     */
    public function CBtouchMulti($keyarray = NULL, $expiry = NULL) {
        return $this->conObject->touchMulti($keyarray, $expiry);
    }

    /**
     * Private buildCacheString
     *
     * Takes in a array and then splits it into a string and underscores
     *
     * @param array resetSplit
     * @return string
     */
    private function buildCacheString($restSplit = array()) {

        $count = count($restSplit);
        $string = "";
        foreach ($restSplit as $key => $method) {
            $string .= Inflector::slug($method);
            if ($count - 1 != $key) {
                $string .= "_";
            }
        }
        return strtolower($this->prefix . $string);
    }

}