The simple ODM for Couchbase on PHP

Installs: 61

Dependents: 1

Stars: 18

Watchers: 4

Forks: 4

Open Issues: 3

Language: PHP

0.3.0 2013-01-17 13:50 UTC


Basement is a fast, efficient and helpful wrapper around the Couchbase PHP SDK. While the SDK itself is very flexible, it lacks lots of convenience features that you as a developer may want to have. Instead of working with the lowlevel operations directly, you get the ability to query your data with models and documents. It also provides sensible defaults that allow you to write less verbose code. Of course, all original methods and options can be accessed at any time when needed.

Currently, this library is in a very early stage, but should be usable pretty soon. As more features become available, I'll make sure to improve the documentation at the same time. Feel free to raise any issues, improvements or feature requests directly on GitHub.


Basement depends on the Couchbase PHP SDK, which depends on libcouchbase. Also, you need to have a Couchbase Server 2.0 cluster running (you can use 1.8, but then you won't have support for views). Please refer to their documentation on how to install them (it mainly depends on the operating system that you are using).

Here is a quick example on how to do it on Mac OS X. First, install libcouchbase through homebrew.

shell> brew install libcouchbase

Now, go to the SDK download site, download the archive for Mac and extract it. Then copy the .so file to your extension directory (if you are unsure where, try with php -i | grep extension_dir) and add it to your php.ini with

Finally, you can check with php -m | grep couchbase if it is installed correctly.

As a side note, Basement needs at least PHP 5.3 to work properly. If you are now thinking "I can't use it in my environment, because its older", then you should stop reading this and go upgrading. PHP 5.3 has been around for years and 5.4 is the current stable release. Basement is fully tested on 5.4 and is recommended for production usage.


Basement is available either standalone or through Composer. If you want to use it standalone, make sure you have an appropriate PSR-0 autoloader around (most modern frameworks provide one). If you use Composer, you can use its autoloader as well.

Add this to your composer.json:

    "require": {
        "basement/basement": "0.3.0"

You can now run composer.phar to install the dependency:

shell> composer.phar update

If you want to use the Composer autoloader in your code, add this line somewhere in your bootstrap code:

require 'vendor/autoload.php';

For more information regarding Composer, please consult their documentation.

Connecting to your Cluster

The Connection and the lower level abstractions are handled through the Basement\Client class. The Client object therefore is the main entry point when talking to your Couchbase cluster.

The easiest way to open a connection is to use the default settings:

use Basement\Client;
$client = new Client();

If you don't provide any further parameters, it will try to connect to and will use the default bucket. You can use this settings if you're developing locally or just starting out with Couchbase. You can also pass in an array of options, which can override the default settings. Here is the array of default settings that you can override as needed:

$defaults = array(
    'name' => 'default',
    'host' => '',
    'bucket' => 'default',
    'password' => '',
    'user' => null,
    'persist' => false,
    'connect' => true,
    'transcoder' => 'json'

Note that for host, you can also pass in an array of hosts to connect to or a ;-delimited string. For example, if you want to connect to the beer-sample bucket on you can use the following code:

use Basement\Client;
$client = new Client(array('host' => '', 'bucket' => 'beer-sample'));

You can then check with $client->connected() if the connection was successful. If the client could not connect to the cluster, it will raise a RuntimeException with the corresponding error message from the SDK.

You can also get access to the underlying SDK client object through the connection() method after the connection has been established. You can use it to issue every command you like against the SDK.

use Basement\Client;
$client = new Client();
$client->connection()->increment("mycounter", 1);

The transcoder setting defines in which format the data will be encoded/decoded when written/read to/from the cluster. The default setting is json, but you can either provide serialize or your own ones (see the advanced topics). You can also override this setting on a per-query-basis.

Managing Connections

The Client class allows you to open more connections at the same time and keeps track of them all. Every connection needs to have a unique name, the default is default. You can override it on connect with the name param:

$backup = new \Basement\Client(array('name' => 'backup'));

You can get your connections all the time by calling the static connections method and passing on the name:

$backup = Client::connections('backup');

If you call disconnect() on the object, it will remove it from the connections array. Important is to note that the name of the connection must be unique or it will override the previous connection. This also applies to the default name, which will override it when you don't pass a name param at all.

You will later see how to use this functionality on your models to define different connections on a per-model basis.

Storing Documents

Basement will do its best to transform the data you want to store into JSON. To store whole documents, you can use the save() method on the Basement\Client object. Currently, there are two ways of passing a document to the save() method: either through an array in the format of array('key' => $stringKey, 'doc' => $anyDoc) or by using instances of the Basement\data\Document object. The latter is recommended since it provides much more flexibility on the document handling and more features will be added to it in the future.

Here is a quick example on how to store the same array as JSON in two different ways:

use Basement\Client;
use Basement\data\Document;

$client = new Client();

$key = 'sampledocument';
$doc = array('store' => 'me', 'please');

// Through an array
$arrayDoc = compact('key', 'doc');

// Through the Document object
$objectDoc = new Document(compact('key', 'doc'));

In this simple example its not obvious why the Basement\data\Document is preferred, but (as shown later) it provides much more convenience features like automatic key generation for free.

The save() method has a bunch of options that you can set, with sensible defaults already set:

$defaults = array(
    'override' => true,
    'replace' => false,
    'transcoder' => 'json',
    'expiration' => 0,
    'cas' => '0'

By overriding them, you can control the behavior how the document is stored and which underlying operations are used. For example, if you set override to false, it will use the add operation instead of the set operation and will return false if the document already exists. You can also set transcoder to serialize if you want PHP object serialization instead of JSON documents, but keep in mind that you are not able to use the full power of views on these documents then. Also, you have to take extra care when fetching those documents out of Couchbase because a json_decode would fail of course.

If either the Document or the array() are somehow invalid or not well-formatted, a InvalidArgumentExceptionis raised with a proper error message.

Retrieving Documents

Documents can either be retrieved by key or through a view. Handling is nearly the same from a client perspective, so working with views is discussed later on. This part focuses on the find() method and all its variants.

The findByKey() and findByView()methods are the easiest way to read a document (or a collection of documents) out of your Couchbase cluster. The default behavior will convert them from JSON to instances of Basement\data\Document, but you can also use serialization or raw data if you prefer to.

Here is an example which stores a document and then fetches it back out again:

use Basement\Client;
use Basement\data\Document;

$client = new Client();

$document = new Document(array(
    'key' => 'my_blogpost',
    'doc' => array(
        'title' => 'This is my first posting',
        'content' => 'Every blog has a first post, so this is mine...'


$documents = $client->findByKey($document->key());

foreach($documents as $doc) {
    // Prints "my_blogpost"
    echo $doc->key();

    // Contains the stored array
    echo $doc->doc();

    // Holds the associated CAS value
    echo $doc->cas();   

If you prefer to work with the raw result instead of having it shuffled into an instance of Basement\data\Document, then you can use the 'raw' => true option. Also, if you've previously stored serialized documents instead of JSON, you can use 'transcoder' => 'serialize' so that it will use unserialize() instead of json_decode().

If you pass in an array of keys, the collection returned contains more than one document. Behind the scenes, the more efficient getMulti() method is used instead of the normal get() method. On the other hand, if you are sure that you only want one document back you can add the "first" => true option. This will return the first document of the collection:

// Longer version:
$documents = $client->findByKey("mykey");
$document = $documents[0];

// Shorter version:
$document = $client->findByKey("mykey", array('first' => true));

Passing in more than one key is easy:

$documents = $client->findByKey(array("key1", "key2"));

Both the findByKey() and findByView() methods are convenience wrappers around the find() method, which you can call directly as well (for example if you want to provide your own abstractions on top of it).

// Those two statemens are equal:
$client->find('key', array('key' => $key));

// You can still pass options like this:
$client->find('key', array('key' => $key, 'transcoder' => 'serialize'));
$client->findByKey($key, array('transcoder' => 'serialize'));

Working with Documents

Instances of \Basement\data\Document ideally represent your documents stored in Couchbase Server. When data is fetched out of the cluster, those objects are created on the fly and they also provide easy handling when storing them back.

Every document provides four "meta" methods, which allow you to read and manipulate settings of the document. These are:

  • key(): Contains the unique key of the document.
  • cas(): Contains the CAS value of the document.
  • doc(): Contains the value of the document itself.
  • value(): The value, which may be populated during view queries.

Depending on how you interact with the cluster, not every attribute is set all the time. Aside from these four methods, there are two that let you work with the stored data itself:

  • set($key, $value): Sets the key with the value inside the document.
  • get($key): Returns the value for the given key.

Here is a quick example on how to work with it:

use \Basement\data\Document;
$doc = new Document();

$doc->set('firstname', 'Michael');
$doc->get('firstname'); // Returns 'Michael'

When stored in the cluster, the JSON document would look like {"firstname":"Michael"}. You can also make use of the property overloading methods __get() and __set() like this to make it even more elegant:

use \Basement\data\Document;
$doc = new Document();

$doc->firstname = 'Michael';
echo $doc->firstname; // Returns 'Michael'

Most of the time, a bunch of documents will be returned. In order to make it easer for you to work with them, they are not plain arrays but instances of Basement\data\DocumentCollection. You can work with them like arrays, but they also provide iterator-like functionality:

// Read Documents from a view
$result = $client->findByView('posts', 'all', $query);
$documents = $result->get();

// The amount of the returned documents.

// Walk forwards and/or backwards between the documents.

Of course, you can still use array-like access and foreach loops.

Working with Views

Working with views is naturally a little bit different than querying by a unique key. While the process "behind the scenes" is completely different, both the SDK and Basement are trying to keep the interface as uniform as possible. Please refer to the official Couchbase Server 2.0 documentation on how to create design documents and views.

Here is a full example on how to query views:

use Basement\Client;

$client = new Client();
$viewResult = $client->findByView("designName", "viewName");

foreach($viewResult->get() as $document) {
    echo $document->key();

The findByView() method returns an instance of Basement\view\ViewResult. The get() method provides instances of \Basement\data\Document objects (similar to key-based get operations). If none are found, an empty collection is returned. This makes it easy to iterate over it and not run into null errors.

Couchbase views can be queried with a large variety of parameters to customize the output. These arguments can either be passed in as an array or by using the \Basement\view\Query object. The latter is preferred because the object only allows you to set the correct params and checks for programming and logic errors. If you use a plain array, you are on your own. Here is a short example on how to use the query parameters:

use Basement\Client;
use Basement\view\Query;

$client = new Client();

$arrayQuery = array('reduce' => 'false', 'include_docs' => 'true');
$viewResult = $client->findByView("design", "view", $arrayQuery);

$objectQuery = new Query();
$viewResult = $client->findByView("design", "view", $objectQuery);

You can see that the Query object allows you to chain params and also handles the conversion from booleans to strings for you. See the API documentation for the Query class and the Couchbase Server 2.0 Manual on Views for more information on what is supported.

If you don't use a reduce function and you set includeDocs to true, the appropriate payload will be automatically populated into the Documentobjects.

Couchbase Server 2.0 allows you to separate Views into development and production Views. This has several benefits, including that you don't have to index your whole dataset while developing. The convention is that views starting with dev_ in the design document name are considered development views. Basement assists you with that and adds the prefix only when needed. On client initialization, you can pass in an environment variable (which defaults to development). Only when it is set to development, Basement prefixes your design document names with dev_. This means that you can deploy the same code in test, development and production and you don't have to worry about it at all. The param is also overridable on a per-query basis.

// Prefixed with `dev_`
$client = new Client();

// Not prefixed with `dev_`
$client = new Client(array('environment' => 'production'));

// Override per query
$client->findByView("design", "view", array('environment' => 'test'));

This solution also integrates nicely with modern frameworks that allow you to set a specific environment based on params like $_SERVER or $_ENV.

Of course, there is also the more verbose find() method available:

// These two method calls are the same:
$client->findByView('myDesign', 'myView', $arrayQuery);
$client->find('view', array('design' => 'myDesign', 'view' => 'myView', 'query' => $arrayQuery));

The ViewResult object allows you to check with isReduced() if it is reduced or not. If it is a reduced result, then the collection of documents don't contain documents with only values (because reduced results can't be mapped to documents one by one). You can still iterate over it and read the results:

$query = new Query();
$viewResult = $client->findByView('design', 'view', $query);

foreach($viewResult->get() as $reducedDoc) {
    echo $reducedDoc->value();

Advanced Usage

The following topics are not needed for everyday use but can come in handy when you need advanced functionality.

Providing custom transcoders

By default, Basement provides transcoders for json and serialize. If you want, you can easily add your own (or override default ones). A transcoder needs to provide both a encode and a decode callable, which is called on every document. Note that on encode, you may want to check if the given data is an array or an instance of \Basement\data\Document, since this would change the way you interact with the object. See the implementation for json or serialize for an example. Here is how to provide a custom transcoder (this one just passes the data through, but you can do whatever you like with it in the callbacks):

$custom = array(
    // called on save() for example
    'encode' => function($input) {
        // your code here.
        return $input;
    // called on find() for example
    'decode' => function($input) {
        // your code here
        return $input;
$this->_client->transcoder('custom', $custom);

Here is the default JSON transcoder for reference:

'json' => array(
    'encode' => function($input) {
        if($input instanceof \Basement\data\Document) {
            return $input->toJson();
        } else { 
            return json_encode($input['doc']);
    'decode' => function($input) {
        return json_decode($input, true);


Here is the rough roadmap towards 1.0 (in the order of when they will be implemented):

  • Basic model functionality (Create, Save, Update, Delete, Proxy find methods, Access to multiple connections from a model-property, Automatic type-setting)
  • Enhanced Query-class tests (may be delivered by the SDK anyway)
  • Design Document Management (adding, listing, deleting)
  • Pagination for View quries
  • Bucket Management (mapper for SDK-provided API)
  • View Migrations

Of course, documentation and general testing support is always included. If you have any suggestion on this, let us know!

Contributing & Support

If you want to hack on Basement, make sure you run Composer with --dev to have the development dependencies (like PHPUnit) installed. You can then invoke the test suite like this:

shell> vendor/bin/phpunit tests/

If you have your cluster running somewhere else, make sure to override the $_testConfig variable in the ClientTest.php. I know this is not the best way to do it, so I'll provide a more flexible way (through a config file) soon.