Author: rick
Date: 2010-03-16 09:40:04 +0100 (Tue, 16 Mar 2010)
New Revision: 28546
Added:
plugins/sfCouchPlugin/trunk/config/
plugins/sfCouchPlugin/trunk/config/config.php
plugins/sfCouchPlugin/trunk/config/couchdb.yml
plugins/sfCouchPlugin/trunk/config/couchdb/
plugins/sfCouchPlugin/trunk/config/couchdb/all_map.js
plugins/sfCouchPlugin/trunk/lib/
plugins/sfCouchPlugin/trunk/lib/sfCouchConnection.class.php
plugins/sfCouchPlugin/trunk/lib/sfCouchDocument.class.php
plugins/sfCouchPlugin/trunk/lib/sfCouchResponse.class.php
plugins/sfCouchPlugin/trunk/lib/sfCouchView.class.php
Log:
Initial checkin
Added: plugins/sfCouchPlugin/trunk/config/config.php
===================================================================
--- plugins/sfCouchPlugin/trunk/config/config.php
(rev 0)
+++ plugins/sfCouchPlugin/trunk/config/config.php 2010-03-16 08:40:04 UTC
(rev 28546)
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @package sfCouchPlugin
+ */
+
+$configCache = $this->getConfigCache();
+
+$couch_config_file = sfConfig::get('sf_config_dir').'/couchdb.yml';
+$configCache->registerConfigHandler($couch_config_file,
'sfDefineEnvironmentConfigHandler', array (
+ 'prefix' => 'couchdb_',
+));
+if ($file = $configCache->checkConfig($couch_config_file, true)) {
+ include($file);
+}
+
Property changes on: plugins/sfCouchPlugin/trunk/config/couchdb/all_map.js
___________________________________________________________________
Added: svn:mime-type
+ text/plain
Added: plugins/sfCouchPlugin/trunk/config/couchdb.yml
===================================================================
--- plugins/sfCouchPlugin/trunk/config/couchdb.yml
(rev 0)
+++ plugins/sfCouchPlugin/trunk/config/couchdb.yml 2010-03-16 08:40:04 UTC
(rev 28546)
@@ -0,0 +1,4 @@
+all:
+ host: localhost
+ port: 5984
+ database: test
\ No newline at end of file
Added: plugins/sfCouchPlugin/trunk/lib/sfCouchConnection.class.php
===================================================================
--- plugins/sfCouchPlugin/trunk/lib/sfCouchConnection.class.php
(rev 0)
+++ plugins/sfCouchPlugin/trunk/lib/sfCouchConnection.class.php 2010-03-16
08:40:04 UTC (rev 28546)
@@ -0,0 +1,372 @@
+<?php
+/**
+ * Basic couch DB connection handling class.
+ *
+ * This class uses a custom HTTP client, which may have more bugs then the
+ * default PHP HTTP clients, but supports keep alive connections without any
+ * extension dependecies.
+ *
+ * @package Core
+ * @version $Revision: 97 $
+ * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPL
+ */
+class sfCouchConnection
+{
+ /**
+ * Connection pointer for connections, once keep alive is working on the
+ * CouchDb side.
+ *
+ * @var resource
+ */
+ protected $connection;
+
+/**
+ * CouchDB connection options
+ *
+ * @var array
+ */
+ protected $options = array(
+ 'timeout' => .1,
+ 'keep-alive' => true,
+ 'http-log' => false,
+ );
+
+ /**
+ * Instance of sfCouchConnection for singleton implementation.
+ *
+ * @var sfCouchConnection
+ */
+ protected static $instance = null;
+
+ /**
+ * Array containing the list of allowed HTTP methods to interact with couch
+ * server.
+ *
+ * @var array
+ */
+ protected static $allowedMethods = array(
+ 'DELETE' => true,
+ 'GET' => true,
+ 'POST' => true,
+ 'PUT' => true,
+ );
+
+ /**
+ * Construct a couch DB connection
+ *
+ * Construct a couch DB connection from basic connection parameters for one
+ * given database. Method is protected and should not be called directly.
+ * For initializing a connection use the static method createInstance().
+ *
+ * @param string $host
+ * @param int $port
+ * @return sfCouchConnection
+ */
+ protected function __construct()
+ {
+ $this->options['host'] = sfConfig::get('couchdb_host', 'localhost');
+ $this->options['port'] = (int) sfConfig::get('couchdb_port', 5984);
+ $this->options['database'] = '/' . sfConfig::get('couchdb_database',
'test') . '/';
+ $this->options['ip'] = gethostbyname($this->options['host']);
+ }
+
+ /**
+ * Set option value
+ *
+ * Set the value for an connection option. Throws an
+ * sfCouchOptionException for unknown options.
+ *
+ * @param string $option
+ * @param mixed $value
+ * @return void
+ */
+ public function setOption( $option, $value )
+ {
+ switch ( $option )
+ {
+ case 'keep-alive':
+ $this->options[$option] = (bool) $value;
+ break;
+
+ case 'http-log':
+ $this->options[$option] = $value;
+ break;
+
+ default:
+ throw new sFException( $option );
+ }
+ }
+
+
+ /**
+ * Get configured couch DB connection instance
+ *
+ * Get configured couch DB connection instance
+ *
+ * @return sfCouchConnection
+ */
+ public static function getInstance()
+ {
+ // Check if connection has been properly confugured, and bail out
+ // otherwise.
+ if ( self::$instance === null )
+ {
+ self::$instance = new sfCouchConnection();
+ }
+
+ // If a connection has been configured properly, jsut return it
+ return self::$instance;
+ }
+
+ /**
+ * HTTP method request wrapper
+ *
+ * Wraps the HTTP method requests to interact with teh couch server. The
+ * supported methods are:
+ * - GET
+ * - DELETE
+ * - POST
+ * - PUT
+ *
+ * Each request takes the request path as the first parameter and
+ * optionally data as the second parameter. The requests will return a
+ * object wrapping the server response.
+ *
+ * @param string $method
+ * @param array $params
+ * @return sfCouch...
+ */
+ public function __call( $method, $params )
+ {
+ // Check if request method is an allowed HTTP request method.
+ $method = strtoupper( $method );
+ if ( !isset( self::$allowedMethods[$method] ) )
+ {
+ throw new sfException('Unsupported request method: ' . $method);
+ }
+
+ // Check if required parameter containing the path is set and valid.
+ if ( $params[0][0] == '{')
+ {
+ $path = $this->options['database'];
+ $data = ( ( isset( $params[0] ) ) ? (string) $params[0] : null );
+ }
+ else {
+ $path = $this->options['database'] . $params[0];
+ $data = ( ( isset( $params[1] ) ) ? (string) $params[1] : null );
+ }
+
+ // Finally perform request and return the result from the server
+ return $this->request( $method, $path, $data );
+ }
+
+ /**
+ * Check for server connection
+ *
+ * Checks if the connection already has been established, or tries to
+ * establish the connection, if not done yet.
+ *
+ * @return void
+ */
+ protected function checkConnection()
+ {
+ // If the connection could not be established, fsockopen sadly does not
+ // only return false (as documented), but also always issues a warning.
+ if ( ( $this->connection === null ) &&
+ ( ( $this->connection = fsockopen( $this->options['ip'],
$this->options['port'], $errno, $errstr ) ) === false ) )
+ {
+ // This is a bit hackisch...
+ throw new sfException("Could not connect to couchdb server");
+ }
+ }
+
+ /**
+ * Build a HTTP 1.1 request
+ *
+ * Build the HTTP 1.1 request headers from the gicven input.
+ *
+ * @param string $method
+ * @param string $path
+ * @param string $data
+ * @return string
+ */
+ protected function buildRequest( $method, $path, $data )
+ {
+ // Create basic request headers
+ $request = "$method $path HTTP/1.1\r\nHost:
{$this->options['host']}\r\n";
+
+ // Set keep-alive header, which helps to keep to connection
+ // initilization costs low, especially when the database server is not
+ // available in the locale net.
+ $request .= "Connection: " . ( $this->options['keep-alive'] ?
'Keep-Alive' : 'Close' ) . "\r\n";
+
+ // Also add headers and request body if data should be sent to the
+ // server. Otherwise just add the closing mark for the header section
+ // of the request.
+ if ( $data !== null )
+ {
+ $request .= "Content-type: application/json\r\n";
+ $request .= "Content-Length: " . strlen( $data ) . "\r\n\r\n";
+ $request .= "$data\r\n";
+ }
+ else
+ {
+ $request .= "\r\n";
+ }
+
+ return $request;
+ }
+
+ /**
+ * Perform a request to the server and return the result
+ *
+ * Perform a request to the server and return the result converted into a
+ * sfCouchResponse object. If you do not expect a JSON structure, which
+ * could be converted in such a response object, set the forth parameter to
+ * true, and you get a response object retuerned, containing the raw body.
+ *
+ * @param string $method
+ * @param string $path
+ * @param string $data
+ * @param bool $raw
+ * @return sfCouchResponse
+ */
+ protected function request( $method, $path, $data)
+ {
+ // Try establishing the connection to the server
+ $this->checkConnection();
+
+ // Send the build request to the server
+ if ( fwrite( $this->connection, $request = $this->buildRequest(
$method, $path, $data ) ) === false )
+ {
+ // Reestablish which seems to have been aborted
+ //
+ // The recursion in this method might be problematic if the
+ // connection establishing mechanism does not correctly throw an
+ // exception on failure.
+ $this->connection = null;
+ return $this->request( $method, $path, $data );
+ }
+
+ // If requested log request information to http log
+ if ( $this->options['http-log'] !== false )
+ {
+ $fp = fopen( $this->options['http-log'], 'a' );
+ fwrite( $fp, "\n\n" . $request );
+ }
+
+ // Read server response headers
+ $rawHeaders = '';
+ $headers = array(
+ 'connection' => ( $this->options['keep-alive'] ? 'Keep-Alive' :
'Close' ),
+ );
+
+ // Remove leading newlines, should not accur at all, actually.
+ while ( ( ( $line = fgets( $this->connection ) ) !== false ) &&
+ ( ( $lineContent = rtrim( $line ) ) === '' ) );
+
+ // Thow exception, if connection has been aborted by the server, and
+ // leave handling to the user for now.
+ if ( $line === false )
+ {
+ throw new sfException( 'Connection abborted unexpectedly
(nonexisting Database?)');
+ }
+
+ do {
+ // Also store raw headers for later logging
+ $rawHeaders .= $lineContent . "\n";
+
+ // Extract header values
+ if ( preg_match(
'(^HTTP/(?P<version>\d+\.\d+)\s+(?P<status>\d+))S', $lineContent, $match ) )
+ {
+ $headers['version'] = $match['version'];
+ $headers['status'] = (int) $match['status'];
+ }
+ else
+ {
+ list( $key, $value ) = explode( ':', $lineContent, 2 );
+ $headers[strtolower( $key )] = ltrim( $value );
+ }
+ } while ( ( ( $line = fgets( $this->connection ) ) !== false ) &&
+ ( ( $lineContent = rtrim( $line ) ) !== '' ) );
+
+ // Read response body
+ $body = '';
+ if ( !isset( $headers['transfer-encoding'] ) ||
+ ( $headers['transfer-encoding'] !== 'chunked' ) )
+ {
+ // HTTP 1.1 supports chunked transfer encoding, if the according
+ // header is not set, just read the specified amount of bytes.
+ $bytesToRead = (int) ( isset( $headers['content-length'] ) ?
$headers['content-length'] : 0 );
+
+ // Read body only as specified by chunk sizes, everything else
+ // are just footnotes, which are not relevant for us.
+ while ( $bytesToRead > 0 )
+ {
+ $body .= $read = fgets( $this->connection, $bytesToRead + 1 );
+ $bytesToRead -= strlen( $read );
+ }
+ }
+ else
+ {
+ // When transfer-encoding=chunked has been specified in the
+ // response headers, read all chunks and sum them up to the body,
+ // until the server has finished. Ignore all additional HTTP
+ // options after that.
+ do {
+ $line = rtrim( fgets( $this->connection ) );
+
+ // Get bytes to read, with option appending comment
+ if ( preg_match( '(^([0-9a-f]+)(?:;.*)?$)', $line, $match ) )
+ {
+ $bytesToRead = hexdec( $match[1] );
+
+ // Read body only as specified by chunk sizes, everything
else
+ // are just footnotes, which are not relevant for us.
+ $bytesLeft = $bytesToRead;
+ while ( $bytesLeft > 0 )
+ {
+ $body .= $read = fread( $this->connection, $bytesLeft
+ 2 );
+ $bytesLeft -= strlen( $read );
+ }
+ }
+ } while ( $bytesToRead > 0 );
+
+ // Chop off \r\n from the end.
+ $body = substr( $body, 0, -2 );
+ }
+
+ // Reset the connection if the server asks for it.
+ if ( $headers['connection'] !== 'Keep-Alive' )
+ {
+ fclose( $this->connection );
+ $this->connection = null;
+ }
+
+ // If requested log response information to http log
+ if ( $this->options['http-log'] !== false )
+ {
+ fwrite( $fp, "\n" . $rawHeaders . "\n" . $body . "\n" );
+ fclose( $fp );
+ }
+
+ // Handle some response state as special cases
+ switch ( $headers['status'] )
+ {
+ case 301:
+ case 302:
+ case 303:
+ case 307:
+ $path = parse_url( $headers['location'], PHP_URL_PATH );
+ return $this->request( $method, $path, $data );
+ break;
+ case 404:
+ return null;
+ break;
+ }
+
+ // Create repsonse object from couch db response
+ return sfCouchResponse::parse( $headers, $body);
+ }
+}
+
Added: plugins/sfCouchPlugin/trunk/lib/sfCouchDocument.class.php
===================================================================
--- plugins/sfCouchPlugin/trunk/lib/sfCouchDocument.class.php
(rev 0)
+++ plugins/sfCouchPlugin/trunk/lib/sfCouchDocument.class.php 2010-03-16
08:40:04 UTC (rev 28546)
@@ -0,0 +1,436 @@
+<?php
+/**
+ * Basic document
+ *
+ * @package Core
+ * @version $Revision: 97 $
+ * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPL
+ */
+class sfCouchDocument
+{
+ /**
+ * Object storing all the document properties as public attributes. This
+ * way it is easy to serialize using json_encode.
+ *
+ * @var StdClass
+ */
+ protected $storage;
+
+ /**
+ * List of required properties. For each required property, which is not
+ * set, a validation exception will be thrown on save.
+ *
+ * @var array
+ */
+ protected $requiredProperties = array();
+
+ /**
+ * Indicates wheather to keep old revisions of this document or not.
+ *
+ * @var bool
+ */
+ protected $versioned = false;
+
+ /**
+ * Flag, indicating if current document has already been modified
+ *
+ * @var bool
+ */
+ protected $modified = false;
+
+ /**
+ * Flag, indicating if current document is a new one.
+ *
+ * @var bool
+ */
+ protected $newDocument = true;
+
+ /**
+ * List of special properties, which are available beside the document
+ * specific properties.
+ *
+ * @var array
+ */
+ protected static $specialProperties = array(
+ '_id',
+ '_rev',
+ '_attachments',
+ 'revisions',
+ );
+
+ /**
+ * List of new attachements to the document.
+ *
+ * @var array
+ */
+ protected $newAttachments = array();
+
+ /**
+ * Set this before calling static functions.
+ *
+ * @var string
+ */
+ public static $docType = null;
+
+ /**
+ * Construct new document
+ *
+ * Construct new document
+ *
+ * @return void
+ */
+ public function __construct($id = null)
+ {
+ $this->storage = new StdClass();
+ $this->storage->revisions = array();
+ $this->storage->_id = null;
+ $this->storage->_attachments = array();
+
+ if ($id) {
+ $this->storage->_id = $id;
+ $this->fetchById($id);
+ }
+ }
+
+ /**
+ * Get document property
+ *
+ * Get property from document
+ *
+ * @param string $property
+ * @return mixed
+ */
+ public function __get( $property )
+ {
+ return $this->storage->$property;
+ }
+
+ /**
+ * Set a property value
+ *
+ * Set a property value, which will be validated using the assigned
+ * validator. Setting a property will mark the document as modified, so
+ * that you know when to store the object.
+ *
+ * @param string $property
+ * @param mixed $value
+ * @return void
+ */
+ public function __set( $property, $value )
+ {
+ $this->storage->$property = $value;
+ $this->modified = true;
+ }
+
+ /**
+ * Check if document property is set
+ *
+ * Check if document property is set
+ *
+ * @param string $property
+ * @return boolean
+ */
+ public function __isset( $property )
+ {
+ // Check if property exists as a custom document property
+ if ( array_key_exists( $property, $this->properties ) ||
+ in_array( $property, self::$specialProperties ) )
+ {
+ return true;
+ }
+
+ // If none of the above checks passed, the request is invalid.
+ return false;
+ }
+
+ /**
+ * Set values from a response object
+ *
+ * Set values of the document from the response object, if they are
+ * available in there.
+ *
+ * @param sfCouchResponse $response
+ * @return void
+ */
+ protected function fromResponse( sfCouchResponse $response )
+ {
+ // Set all document property values from response, if available in the
+ // response.
+ //
+ // Also fill a revision object with the set attributtes, so that the
+ // current revision is also available in history, and it is stored,
+ // when the object is modified and stored again.
+ $revision = new StdClass();
+ $revision->_date = time();
+ foreach ( $response->getFullDocument() as $property => $v )
+ {
+ $this->storage->$property = $v;
+ $revision->$property = $v;
+ }
+
+ /*
+ // Set special properties from response object
+ $this->storage->_rev = $response->_rev;
+ $this->storage->_id = $response->_id;
+
+ // Set attachements array, if the response object contains
attachements.
+ if ( isset( $response->_attachments ) )
+ {
+ $this->storage->_attachments = $response->_attachments;
+ }
+ */
+
+ // Check if the source document already contains a revision history and
+ // store it in this case in the document object, if the object should
+ // be versioned at all.
+ if ( $this->versioned )
+ {
+ if ( isset( $response->revisions ) )
+ {
+ $this->storage->revisions = $response->revisions;
+ }
+ // Add current revision to revision history
+ $this->storage->revisions[] = (array) $revision;
+ }
+ // Document freshly loaded, so it is not modified, and not a new
+ // document...
+ $this->modified = false;
+ $this->newDocument = false;
+ }
+
+ /**
+ * Get document by ID
+ *
+ * Get document by ID and return a document objetc instance for the fetch
+ * document.
+ *
+ * @param string $id
+ * @return sfCouchDocument
+ */
+ public function fetchById( $id )
+ {
+ // If a fetch is called with an empty ID, we throw an exception, as we
+ // would get database statistics otherwise, and the following error may
+ // be hard to debug.
+ if ( empty( $id ) )
+ {
+ throw new sfException('No document ID specified.');
+ }
+
+ // Fetch object from database
+ $db = sfCouchConnection::getInstance();
+ $response = $db->get(
+ urlencode( $id )
+ );
+
+ // Create document contents from fetched object
+ if (!empty($response)) {
+ $this->fromResponse( $response );
+ }
+
+ return $this;
+ }
+
+
+ /**
+ * Get ID from document
+ *
+ * The ID normally should be calculated on some meaningful / unique
+ * property for the current ttype of documents. The returned string should
+ * not be too long and should not contain multibyte characters.
+ *
+ * You can return null instead of an ID string, to trigger the ID
+ * autogeneration.
+ *
+ * @return mixed
+ */
+ protected function generateId()
+ {
+ return null;
+ }
+
+ /**
+ * Check if all requirements are met
+ *
+ * Checks if all required properties has been set. Returns an array with
+ * the properties, whcih are required but not set, or true if all
+ * requirements are fulfilled.
+ *
+ * @return mixed
+ */
+ public function checkRequirements()
+ {
+ // Iterate over properties and check if they are set and not null
+ $errors = array();
+ foreach ( $this->requiredProperties as $property )
+ {
+ if ( !isset( $this->storage->$property ) ||
+ ( $this->storage->$property === null ) )
+ {
+ $errors[] = $property;
+ }
+ }
+
+ // If error array is still empty all requirements are met
+ if ( $errors === array() )
+ {
+ return true;
+ }
+
+ // Otherwise return the array with errors
+ return $errors;
+ }
+
+ /**
+ * Save the document
+ *
+ * If thew document has not been modfied the method will immediatly exit
+ * and return false. If the document has been been modified, the modified
+ * document will be stored in the database, keeping all the old revision
+ * intact and return true on success.
+ *
+ * On successful creation the (generated) ID will be returned.
+ *
+ * @return string
+ */
+ public function save()
+ {
+
+ // Ensure all requirements are checked, otherwise bail out with a
+ // runtime exception.
+ if ( $this->checkRequirements() !== true )
+ {
+ throw new sfException(
+ "Required properties for this document aren't set."
+ );
+ }
+
+ // Check if we need to store the stuff at all
+ if ( ( $this->modified === false ) &&
+ ( $this->newDocument !== true ) )
+ {
+ return false;
+ }
+
+ // Generate a new ID, if this is a new document, otherwise reuse the
+ // existing document ID.
+ if ( $this->storage->_id === null )
+ {
+ $this->storage->_id = $this->generateId();
+ }
+
+ // Do not send an attachment array, if there aren't any attachements
+ if ( !isset( $this->storage->_attachments ) ||
+ !count( $this->storage->_attachments ) )
+ {
+ unset( $this->storage->_attachments );
+ }
+
+ // If the document ID is null, the server should autogenerate some ID,
+ // but for this we need to use a different request method.
+ $db = sfCouchConnection::getInstance();
+ if ( $this->storage->_id === null )
+ {
+ // Store document in database
+ unset( $this->storage->_id );
+ $response = $db->post(
+ json_encode( $this->storage )
+ );
+ }
+ else
+ {
+ // Store document in database
+ $response = $db->put(
+ urlencode( $this->_id ),
+ json_encode( $this->storage )
+ );
+ }
+
+ print_r($response);
+
+ if (empty($response)) {
+ return null;
+ }
+ return $this->storage->_id = $response->id;
+ }
+
+ /**
+ * Get ID string from arbritrary string
+ *
+ * To calculate an ID string from an sfCouchrary string, first iconvs
+ * tarnsliteration abilities are used, and after that all, but common ID
+ * characters, are replaced by the given replace string, which defaults to
+ * _.
+ *
+ * @param string $string
+ * @param string $replace
+ * @return string
+ */
+ protected function stringToId( $string, $replace = '_' )
+ {
+ // First translit string to ASCII, as this characters are most probably
+ // supported everywhere
+ $string = iconv( 'UTF-8', 'ASCII//TRANSLIT', $string );
+
+ // And then still replace any obscure characers by _ to ensure nothing
+ // "bad" happens with this string.
+ $string = preg_replace( '([^A-Za-z0-9.-]+)', $replace, $string );
+
+ // Additionally we convert the string to lowercase, so that we get case
+ // insensitive fetching
+ return strtolower( $string );
+ }
+
+ /**
+ * Attach file to document
+ *
+ * The file passed to the method will be attached to the document and
+ * stored in the database. By default the filename of the provided file
+ * will be ued as a name, but you may optionally specify a name as the
+ * second parameter of the method.
+ *
+ * You may optionally specify a custom mime type as third parameter. If set
+ * it will be used, but not verified, that it matches the actual file
+ * contents. If left empty the mime type defaults to
+ * 'application/octet-stream'.
+ *
+ * @param string $fileName
+ * @param string $name
+ * @param string $mimeType
+ * @return void
+ */
+ public function attachFile( $fileName, $name = false, $mimeType = false )
+ {
+ $name = ( $name === false ? basename( $fileName ) : $name );
+ $this->storage->_attachments[$name] = array(
+ 'type' => 'base64',
+ 'data' => base64_encode( file_get_contents( $fileName ) ),
+ 'content_type' => $mimeType === false ? 'application/octet-stream'
: $mimeType,
+ );
+ $this->modified = true;
+ }
+
+ /**
+ * Get file contents
+ *
+ * Get the contents of an attached file as a sfCouchDataResponse.
+ *
+ * @param string $fileName
+ * @return sfCouchDataResponse
+ */
+ public function getFile( $fileName )
+ {
+ if ( !isset( $this->storage->_attachments[$fileName] ) )
+ {
+ throw new sfException( $fileName );
+ }
+
+ $db = sfCouchConnection::getInstance();
+ $response = $db->get(
+ urlencode( $this->_id ) . '/' . $fileName,
+ null, true
+ );
+
+ return $response;
+ }
+}
\ No newline at end of file
Added: plugins/sfCouchPlugin/trunk/lib/sfCouchResponse.class.php
===================================================================
--- plugins/sfCouchPlugin/trunk/lib/sfCouchResponse.class.php
(rev 0)
+++ plugins/sfCouchPlugin/trunk/lib/sfCouchResponse.class.php 2010-03-16
08:40:04 UTC (rev 28546)
@@ -0,0 +1,154 @@
+<?php
+/**
+ * Response factory to create response objects from JSON results
+ *
+ * @package Core
+ * @version $Revision: 44 $
+ * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPL
+ */
+class sfCouchResponse
+{
+ /**
+ * Array containing all response properties
+ *
+ * @var array
+ */
+ protected $properties;
+
+ /**
+ * Construct response object from JSON result
+ *
+ * @param array $body
+ * @return void
+ */
+ public function __construct( array $body, $fromArray = false)
+ {
+ if ($fromArray) {
+ $this->properties['data'] = $response;
+ }
+ else {
+ // Set all properties as virtual readonly repsonse object properties.
+ foreach ( $body as $property => $value )
+ {
+ $this->properties[$property] = $value;
+ }
+ }
+ }
+
+ /**
+ * Get full document
+ *
+ * @return array
+ */
+ public function getFullDocument()
+ {
+ return $this->properties;
+ }
+
+ /**
+ * Get available property
+ *
+ * Receive response object property, if available. If the property is not
+ * available, the method will throw an exception.
+ *
+ * @param string $property
+ * @return mixed
+ */
+ public function __get( $property )
+ {
+ // Check if such an property exists at all
+ if ( !isset( $this->properties[$property] ) )
+ {
+ throw new sfException( $property );
+ }
+
+ return $this->properties[$property];
+ }
+
+ /**
+ * Check if property exists.
+ *
+ * Check if property exists.
+ *
+ * @param string $property
+ * @return bool
+ */
+ public function __isset( $property )
+ {
+ return isset( $this->properties[$property] );
+ }
+
+ /**
+ * Silently ignore each write access on response object properties.
+ *
+ * @param string $property
+ * @param mixed $value
+ * @return bool
+ */
+ public function __set( $property, $value )
+ {
+ return false;
+ }
+
+ /**
+ * Parse a server response
+ *
+ * Parses a server response depending on the response body and the HTTP
+ * status code.
+ *
+ * For put and delete requests the server will just return a status,
+ * wheather the request was successfull, which is represented by a
+ * sfCouchStatusResponse object.
+ *
+ * For all other cases most probably some error occured, which is
+ * transformed into a sfCouchResponseErrorException, which will be thrown
+ * by the parse method.
+ *
+ * @param array $headers
+ * @param string $body
+ * @return sfCouchResponse
+ */
+ public static function parse( array $headers, $body)
+ {
+ $response = json_decode( $body, true );
+
+ // To detect the type of the response from the couch DB server we use
+ // the response status which indicates the return type.
+ switch ( $headers['status'] )
+ {
+ case 200:
+ // The HTTP status code 200 - OK indicates, that we got a
document
+ // or a set of documents as return value.
+ //
+ // To check wheather we received a set of documents or a single
+ // document we can check for the document properties _id or
+ // _rev, which are always available for documents and are only
+ // available for documents.
+ if ( $body[0] === '[' )
+ {
+ return new sfCouchResponse( $response, true);
+ }
+ elseif ( isset( $response->_id ) || isset( $response->rows ) )
+ {
+ return new sfCouchResponse( $response );
+ }
+
+ // Otherwise fall back to a plain status response. No break.
+
+ case 201:
+ case 202:
+ // The following status codes are given for status responses
+ // depending on the request type - which does not matter here
any
+ // more.
+ return new sfCouchResponse( $response );
+
+ default:
+ // All other unhandled HTTP codes are for now handled as an
error.
+ // This may not be true, as lots of other status code may be
used
+ // for valid repsonses.
+ throw new sfException( $headers['status'] );
+ }
+ }
+}
+
+
Added: plugins/sfCouchPlugin/trunk/lib/sfCouchView.class.php
===================================================================
--- plugins/sfCouchPlugin/trunk/lib/sfCouchView.class.php
(rev 0)
+++ plugins/sfCouchPlugin/trunk/lib/sfCouchView.class.php 2010-03-16
08:40:04 UTC (rev 28546)
@@ -0,0 +1,180 @@
+<?php
+/**
+ * Wrapper base for views in the database
+ *
+ * @package Core
+ * @version $Revision: 94 $
+ * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPL
+ */
+class sfCouchView
+{
+ /*
+ * The ID of the design-view
+ */
+ const viewName = '_design/sfCouch';
+
+ /**
+ * Build view query string from options
+ *
+ * Validates and transformed paased options to limit the view data, to fit
+ * the specifications in the HTTP view API, documented at:
+ *
http://www.couchdbwiki.com/index.php?title=HTTP_View_API#Querying_Options
+ *
+ * @param array $options
+ * @return string
+ */
+ private static function buildViewQuery( array $options )
+ {
+ // Return empty query string, if no options has been passed
+ if ( $options === array() )
+ {
+ return '';
+ }
+
+ $queryString = '?';
+ foreach ( $options as $key => $value )
+ {
+ switch ( $key )
+ {
+ case 'key':
+ case 'startkey':
+ case 'endkey':
+ // These values has to be valid JSON encoded strings, so we
+ // just encode the passed data, whatever it is, as CouchDB
+ // may use complex datatypes as a key, like arrays or
+ // objects.
+ $queryString .= $key . '=' . urlencode( json_encode(
$value ) );
+ break;
+
+ case 'startkey_docid':
+ // The docidstartkey is handled differntly then the other
+ // keys and is just passed as a string, because it always
+ // is and can only be a string.
+ $queryString .= $key . '=' . urlencode( (string) $value );
+ break;
+
+ case 'group':
+ case 'update':
+ case 'descending':
+ // These two values may only contain boolean values, passed
+ // as "true" or "false". We just perform a typical PHP
+ // boolean typecast to transform the values.
+ $queryString .= $key . '=' . ( $value ? 'true' : 'false' );
+ break;
+
+ case 'skip':
+ case 'group_level':
+ // Theses options accept integers defining the limits of
+ // the query. We try to typecast to int.
+ $queryString .= $key . '=' . ( (int) $value );
+ break;
+
+ case 'count': // CouchDB 0.8. compat
+ case 'limit':
+ // Theses options accept integers defining the limits of
+ // the query. We try to typecast to int.
+ $queryString .= 'limit=' . ( (int) $value );
+ break;
+
+ default:
+ throw new sfException( $key );
+ }
+
+ $queryString .= '&';
+ }
+
+ // Return query string, but remove appended '&' first.
+ return substr( $queryString, 0, -1 );
+ }
+
+ /**
+ * Query a view
+ *
+ * Query the specified view to get a set of results. You may optionally use
+ * the view query options as additional paramters to limit the returns
+ * values, specified at:
+ *
http://www.couchdbwiki.com/index.php?title=HTTP_View_API#Querying_Options
+ *
+ * @param string $view
+ * @param array $options
+ * @return sfCouchResultArray
+ */
+ public static function query( $view, array $options = array() )
+ {
+ $response = null;
+
+ // Build query string, just as a normal HTTP GET query string
+ $url = self::viewName . '/_view/' . $view;
+ $url .= self::buildViewQuery( $options );
+
+ // Get database connection, because we directly execute a query here.
+ $db = sfCouchConnection::getInstance();
+
+ // Always refresh the configuration in debug mode
+ if(sfConfig::get('sf_debug')) {
+ self::refreshDesignDoc();
+ }
+
+ try
+ {
+ // Try to execute query, a failure most probably means, the view
+ // has not been added, yet.
+ $response = $db->get( $url );
+ }
+ catch ( sfException $e )
+ {
+ // If we aren't in debug mode Ensure view has been created
properly and then try to execute
+ // the query again. If it still fails, there is most probably a
+ // real problem.
+ if (!sfConfig::get('sf_debug') && self::refreshDesignDoc()) {
+ $response = $db->get($url);
+ }
+ }
+
+ return $response;
+ }
+
+
+ /**
+ * Create the view document
+ *
+ * Check if the views stored in the database equal the view definitions
+ * specified by the vew classes. If the implmentation differs update to the
+ * view specifications in the class.
+ *
+ * @return void
+ */
+ public static function refreshDesignDoc()
+ {
+ $designDoc = new sfCouchDocument(self::viewName);
+
+ $designDoc->language = 'javascript';
+
+ // Build the maps/reduces from the files in the config dir
+ $mapDir = sfConfig::get('sf_config_dir').'/couchdb/';
+ $designDoc->views = self::getViewsFromConfig($mapDir);
+
+ $designDoc->save();
+ }
+
+ private static function getViewsFromConfig($dir)
+ {
+ $views = array();
+ foreach (glob($dir.'*.js') as $fileName) {
+ preg_match('/.*\/(.*)_map.js/', $fileName, $filematches);
+ if (isset($filematches[1])) {
+
+ $viewName = $filematches[1];
+ $views[$viewName] = array();
+ $views[$viewName]['map'] = file_get_contents($fileName);
+
+ $reduceFileName = $dir.$viewName.'_reduce.js';
+ if (file_exists($reduceFileName)) {
+ $views[$viewName]['reduce'] =
file_get_contents($reduceFileName);
+ }
+ }
+ }
+
+ return $views;
+ }
+}
\ No newline at end of file
--
You received this message because you are subscribed to the Google Groups
"symfony SVN" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to
[email protected].
For more options, visit this group at
http://groups.google.com/group/symfony-svn?hl=en.