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.

Reply via email to