Skip to content

Commit

Permalink
Merge pull request doctrine#1 from doctrine/WindowsAzureTable
Browse files Browse the repository at this point in the history
Windows Azure Table
  • Loading branch information
beberlei committed Apr 2, 2012
2 parents 2aae182 + 6047178 commit 8367da2
Show file tree
Hide file tree
Showing 26 changed files with 1,626 additions and 168 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
vendor
*phpunit.xml
44 changes: 42 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The Persistence interfaces are rather overkill for many implementations in the N

Following vendors are targeted:

* Microsoft Azure Table
* Microsoft Azure Table (Implemented)
* Amazon DynamoDB
* CouchDB
* MongoDB
Expand All @@ -36,7 +36,7 @@ Suppose we track e-mail campaigns based on campaign id and recipients.
use Doctrine\KeyValueStore\Mapping\Annotations as KeyValue;

/**
* @KeyValue\Entity
* @KeyValue\Entity(storageName="responses")
*/
class Response
{
Expand Down Expand Up @@ -67,3 +67,43 @@ Suppose we track e-mail campaigns based on campaign id and recipients.

$entityManager->flush();

## Configuration

There is no factory yet that simplifies the creation process, here is the
full code necessary to instantiate a KeyValue EntityManager with a Doctrine
Cache backend:

use Doctrine\KeyValueStore\EntityManager;
use Doctrine\KeyValueStore\Mapping\AnnotationDriver;
use Doctrine\KeyValueStore\Storage\DoctrineCacheStorage;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Annotations\AnnotationReader;

$storage = new DoctrineCacheStorage($cache);
$cache = new ArrayCache;
$metadata = new AnnotationDriver(new AnnotationReader);
$entityManager = new EntityManager($storage, $cache, $metadata);

If you want to use WindowsAzure Table you can use the following configuration
to instantiate the storage:

use Doctrine\KeyValueStore\Storage\WindowsAzureTableStorage;
use Doctrine\KeyValueStore\Storage\WindowsAzureTable\SharedKeyLiteAuthorization;
use Doctrine\KeyValueStore\Http\SocketClient;

$name = ""; // Windows Azure Storage Account Name
$key = ""; // Windows Azure Storage Account Key

$auth = new SharedKeyLiteAuthorization($name, $key);
$storage = new WindowsAzureTableStorage(new SocketClient(), $name, $auth);

If you want to use Doctrine DBAL as backend:

$params = array();
$tableName = "storage";
$idColumnName = "id";
$dataColumnName = "serialized_data";

$conn = DriverManager::getConnection($params);
$storage = new DBALStorage($conn, $tableName, $idColumnName, $dataColumnName);

19 changes: 16 additions & 3 deletions lib/Doctrine/KeyValueStore/EntityManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

use Doctrine\KeyValueStore\Storage\Storage;
use Doctrine\KeyValueStore\Mapping\ClassMetadataFactory;
use Doctrine\KeyValueStore\Query\RangeQuery;
use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Common\Cache\Cache;

Expand Down Expand Up @@ -64,11 +65,11 @@ public function find($className, $key, array $fields = null)
*
* @param string $className
* @param string $partitionKey
* @return Iterator
* @return \Doctrine\KeyValueStore\Query\RangeQuery
*/
public function findRange($className, $partitionKey, array $conditions = array(), array $fields = array(), $limit = null, $offset = null)
public function createRangeQuery($className, $partitionKey)
{

return new RangeQuery($this, $className, $partitionKey);
}

public function persist($object)
Expand All @@ -94,6 +95,18 @@ public function unwrap()
return $this->storageDriver;
}

/**
* @return \Doctrine\KeyValueStore\UnitOfWork
*/
public function getUnitOfWork()
{
return $this->unitOfWork;
}

/**
* @param string $className
* @return \Doctrine\KeyValueStore\Mapping\ClassMetadata
*/
public function getClassMetadata($className)
{
return $this->unitOfwork->getClassMetadata($className);
Expand Down
2 changes: 1 addition & 1 deletion lib/Doctrine/KeyValueStore/Http/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ interface Client
* @param array $headers
* @return Response
*/
function request($method, $url, $body = null, array $headers);
function request($method, $url, $body = null, array $headers = array());
}

250 changes: 250 additions & 0 deletions lib/Doctrine/KeyValueStore/Http/SocketClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.doctrine-project.org>.
*/

namespace Doctrine\KeyValueStore\Http;

/**
* 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 dependencies.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.com
* @since 1.0
* @author Kore Nordmann <kore@arbitracker.org>
*/
class SocketClient implements Client
{
/**
* Connection pointer for connections, once keep alive is working on the
* server side.
*
* @var resource
*/
protected $connection;

/**
* @var array
*/
private $options = array(
'keep-alive' => true,
);

public function __construct(array $options = array())
{
$this->options = array_merge($this->options, $options);
}

/**
* 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($host, $port)
{
$host = ($port == 443) ? "ssl://" . $host : "tcp://" . $host;

// 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 = @stream_socket_client($host . ":" . $port, $errno, $errstr ) ) === false ) )
{
// This is a bit hackisch...
$this->connection = null;
throw new \RuntimeException("fail");
}
}

/**
* 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, $url, $data, $headers )
{
$parts = parse_url($url);
$host = $parts['host'];
$path = $parts['path'];

// Create basic request headers
$request = "$method $path HTTP/1.1\r\nHost: {$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.
foreach ($headers as $name => $header) {
$request .= $name . ": " . $header . "\r\n";
}
$request = rtrim($request) . "\r\n\r\n";

if ( $data !== null ) {
$request .= $data;
}

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
* Response 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 Response
*/
public function request($method, $url, $data = null, array $headers = array())
{
// Try establishing the connection to the server
$parts = parse_url($url);
$host = $parts['host'];
$this->checkConnection($host, $parts['scheme']=='https' ? 443 : 80);

// Send the build request to the server
if ( fwrite( $this->connection, $request = $this->buildRequest( $method, $url, $data, $headers ) ) === 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, $url, $data, $headers );
}

// 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 ) ) === '' ) );

// Throw exception, if connection has been aborted by the server, and
// leave handling to the user for now.
if ( $line === 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.
//
// An aborted connection seems to happen here on long running
// requests, which cause a connection timeout at server side.
$this->connection = null;
return $this->request( $method, $url, $data, $raw );
}

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-fA-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;
}

// Handle some response state as special cases
switch ( $headers['status'] ) {
case 301:
case 302:
case 303:
case 307:
return $this->request( $method, $headers['location'], $data, $raw );
}

return new Response( $headers['status'], $body, $headers );
}
}

Loading

0 comments on commit 8367da2

Please sign in to comment.