├── .gitignore
├── lib
└── Doctrine
│ └── CouchDB
│ ├── Version.php
│ ├── HTTP
│ ├── ErrorResponse.php
│ ├── Response.php
│ ├── Client.php
│ ├── HTTPException.php
│ ├── LoggingClient.php
│ ├── AbstractHTTPClient.php
│ ├── StreamClient.php
│ ├── SocketClient.php
│ └── MultipartParserAndSender.php
│ ├── JsonDecodeException.php
│ ├── Tools
│ ├── Console
│ │ ├── Command
│ │ │ ├── ViewCleanupCommand.php
│ │ │ ├── CompactDatabaseCommand.php
│ │ │ ├── CompactViewCommand.php
│ │ │ ├── MigrationCommand.php
│ │ │ ├── ReplicationCancelCommand.php
│ │ │ └── ReplicationStartCommand.php
│ │ └── Helper
│ │ │ └── CouchDBHelper.php
│ └── Migrations
│ │ └── AbstractMigration.php
│ ├── View
│ ├── DesignDocument.php
│ ├── LuceneResult.php
│ ├── Result.php
│ ├── FolderDesignDocument.php
│ ├── LuceneQuery.php
│ ├── AbstractQuery.php
│ └── Query.php
│ ├── MangoClient.php
│ ├── CouchDBException.php
│ ├── Utils
│ └── BulkUpdater.php
│ ├── Mango
│ └── MangoQuery.php
│ ├── Attachment.php
│ └── CouchDBClient.php
├── tests
├── Doctrine
│ └── Tests
│ │ ├── Models
│ │ └── CMS
│ │ │ └── _files
│ │ │ ├── filters
│ │ │ └── my_filter.js
│ │ │ ├── views
│ │ │ └── username
│ │ │ │ └── map.js
│ │ │ └── rewrites.json
│ │ └── CouchDB
│ │ ├── TestUtil.php
│ │ ├── CouchDBExceptionTest.php
│ │ ├── CouchDBFunctionalTestCase.php
│ │ ├── CouchDBClientTest.php
│ │ └── Functional
│ │ ├── BulkUpdaterTest.php
│ │ ├── MangoQueryTest.php
│ │ ├── MangoTest.php
│ │ ├── HTTP
│ │ └── MultipartParserAndSenderTest.php
│ │ └── CouchDBClientTest.php
└── TestInit.php
├── .travis.yml
├── .doctrine-project.json
├── phpunit.xml.dist
├── composer.json
├── LICENSE
├── README.md
└── docs
└── en
└── index.rst
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | .idea
3 | composer.lock
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/Version.php:
--------------------------------------------------------------------------------
1 |
17 | */
18 | class ErrorResponse extends Response
19 | {
20 | }
21 |
--------------------------------------------------------------------------------
/.doctrine-project.json:
--------------------------------------------------------------------------------
1 | {
2 | "active": false,
3 | "name": "CouchDB Client",
4 | "shortName": "CouchDB Client",
5 | "slug": "couchdb-client",
6 | "docsSlug": "doctrine-couchdb-client",
7 | "versions": [
8 | {
9 | "name": "master",
10 | "branchName": "master",
11 | "slug": "latest",
12 | "aliases": [
13 | "current",
14 | "stable"
15 | ]
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/tests/Doctrine/Tests/CouchDB/TestUtil.php:
--------------------------------------------------------------------------------
1 | setName('couchdb:maintenance:view-cleanup')
14 | ->setDescription('Cleanup deleted views');
15 | }
16 |
17 | protected function execute(InputInterface $input, OutputInterface $output)
18 | {
19 | $couchClient = $this->getHelper('couchdb')->getCouchDBClient();
20 | /* @var $couchClient \Doctrine\CouchDB\CouchDBClient */
21 |
22 | $data = $couchClient->viewCleanup();
23 | $output->writeln('View cleanup started.');
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ./tests/Doctrine/Tests/CouchDB
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "doctrine/couchdb",
3 | "type": "library",
4 | "description": "CouchDB Client",
5 | "keywords": ["persistence", "couchdb"],
6 | "homepage": "http://www.doctrine-project.org",
7 | "license": "MIT",
8 | "authors": [
9 | {"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"},
10 | {"name": "Lukas Kahwe Smith", "email": "smith@pooteeweet.org"}
11 | ],
12 | "require": {
13 | "php": ">=5.4"
14 | },
15 | "require-dev": {
16 | "phpunit/phpunit": "~4.0"
17 | },
18 | "autoload": {
19 | "psr-0": {
20 | "Doctrine\\CouchDB": "lib/"
21 | }
22 | },
23 | "autoload-dev": {
24 | "psr-0": {
25 | "Doctrine\\Tests": "tests/"
26 | }
27 | },
28 | "extra": {
29 | "branch-alias": {
30 | "dev-master": "1.0-dev"
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/Tools/Console/Command/CompactDatabaseCommand.php:
--------------------------------------------------------------------------------
1 | setName('couchdb:maintenance:compact-database')
14 | ->setDescription('Compact the database');
15 | }
16 |
17 | protected function execute(InputInterface $input, OutputInterface $output)
18 | {
19 | $couchClient = $this->getHelper('couchdb')->getCouchDBClient();
20 | /* @var $couchClient \Doctrine\CouchDB\CouchDBClient */
21 |
22 | $data = $couchClient->compactDatabase();
23 | $output->writeln('Database compact started.');
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/View/DesignDocument.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | interface DesignDocument
16 | {
17 | /**
18 | * Get design doc code.
19 | *
20 | * Return the view (or general design doc) code, which should be
21 | * committed to the database, which should be structured like:
22 | *
23 | *
24 | * array(
25 | * "views" => array(
26 | * "name" => array(
27 | * "map" => "code",
28 | * ["reduce" => "code"],
29 | * ),
30 | * ...
31 | * )
32 | * )
33 | *
34 | */
35 | public function getData();
36 | }
37 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/View/LuceneResult.php:
--------------------------------------------------------------------------------
1 | result['etag'];
10 | }
11 |
12 | public function getFetchDuration()
13 | {
14 | return $this->result['fetch_duration'];
15 | }
16 |
17 | public function getLimit()
18 | {
19 | return $this->result['limit'];
20 | }
21 |
22 | public function getExecutedQuery()
23 | {
24 | return $this->result['q'];
25 | }
26 |
27 | public function getRows()
28 | {
29 | return $this->result['rows'];
30 | }
31 |
32 | public function getSearchDuration()
33 | {
34 | return $this->result['search_duration'];
35 | }
36 |
37 | public function getSkip()
38 | {
39 | return $this->result['skip'];
40 | }
41 |
42 | public function getTotalRows()
43 | {
44 | return $this->result['total_rows'];
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2006-2015 Doctrine Project
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
9 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/Tools/Console/Command/CompactViewCommand.php:
--------------------------------------------------------------------------------
1 | setName('couchdb:maintenance:compact-view')
15 | ->setDescription('Compat the given view')
16 | ->setDefinition([
17 | new InputArgument('designdoc', InputArgument::REQUIRED, 'Design document name', null),
18 | ]);
19 | }
20 |
21 | protected function execute(InputInterface $input, OutputInterface $output)
22 | {
23 | $couchClient = $this->getHelper('couchdb')->getCouchDBClient();
24 | /* @var $couchClient \Doctrine\CouchDB\CouchDBClient */
25 |
26 | $data = $couchClient->compactView($input->getArgument('designdoc'));
27 | $output->writeln('View compact started.');
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/HTTP/Response.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class Response
16 | {
17 | /**
18 | * HTTP response status.
19 | *
20 | * @var int
21 | */
22 | public $status;
23 |
24 | /**
25 | * HTTP response headers.
26 | *
27 | * @var array
28 | */
29 | public $headers;
30 |
31 | /**
32 | * Decoded JSON response body.
33 | *
34 | * @var array
35 | */
36 | public $body;
37 |
38 | /**
39 | * Construct response.
40 | *
41 | * @param $status
42 | * @param array $headers
43 | * @param string $body
44 | * @param bool $raw
45 | *
46 | * @return void
47 | */
48 | public function __construct($status, array $headers, $body, $raw = false)
49 | {
50 | $this->status = (int) $status;
51 | $this->headers = $headers;
52 | $this->body = $raw ? $body : json_decode($body, true);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/Tools/Console/Command/MigrationCommand.php:
--------------------------------------------------------------------------------
1 | setName('couchdb:migrate')
15 | ->setDescription('Execute a migration in CouchDB.')
16 | ->setDefinition([
17 | new InputArgument('class', InputArgument::REQUIRED, 'Migration class name', null),
18 | ]);
19 | }
20 |
21 | protected function execute(InputInterface $input, OutputInterface $output)
22 | {
23 | $className = $input->getArgument('class');
24 | if (!class_exists($className) || !in_array('Doctrine\CouchDB\Tools\Migrations\AbstractMigration', class_parents($className))) {
25 | throw new \InvalidArgumentException("class passed to command has to extend 'Doctrine\CouchDB\Tools\Migrations\AbstractMigration'");
26 | }
27 | $migration = new $className($this->getHelper('couchdb')->getCouchDBClient());
28 | $migration->execute();
29 |
30 | $output->writeln('Migration was successfully executed!');
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/View/Result.php:
--------------------------------------------------------------------------------
1 | result = $result;
12 | }
13 |
14 | public function getTotalRows()
15 | {
16 | return $this->result['total_rows'];
17 | }
18 |
19 | public function getIterator()
20 | {
21 | return new \ArrayIterator($this->result['rows']);
22 | }
23 |
24 | public function count()
25 | {
26 | return count($this->result['rows']);
27 | }
28 |
29 | public function offsetExists($offset)
30 | {
31 | return isset($this->result['rows'][$offset]);
32 | }
33 |
34 | public function offsetGet($offset)
35 | {
36 | return $this->result['rows'][$offset];
37 | }
38 |
39 | public function offsetSet($offset, $value)
40 | {
41 | throw new \BadMethodCallException('Result is immutable and cannot be changed.');
42 | }
43 |
44 | public function offsetUnset($offset)
45 | {
46 | throw new \BadMethodCallException('Result is immutable and cannot be changed.');
47 | }
48 |
49 | public function toArray()
50 | {
51 | return $this->result['rows'];
52 | }
53 |
54 | public function getOffset()
55 | {
56 | return $this->result['offset'];
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/tests/Doctrine/Tests/CouchDB/CouchDBExceptionTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($expectedMessage, $exception->getMessage());
19 | }
20 |
21 | public function staticFactoryDataProvider()
22 | {
23 | return [
24 | ['unknownDocumentNamespace', 'Unknown Document namespace alias \'a\'.'],
25 | ['unregisteredDesignDocument', 'No design document with name \'a\' was registered with the DocumentManager.'],
26 | ['invalidAttachment', 'Trying to save invalid attachment with filename c in document a with id b'],
27 | ['detachedDocumentFound', 'Found a detached or new document at property a::c of document with ID b, but the assocation is not marked as cascade persist.'],
28 | ['persistRemovedDocument', 'Trying to persist document that is scheduled for removal.'],
29 | ['luceneNotConfigured', 'CouchDB Lucene is not configured. You have to configure the handler name to enable support for Lucene Queries.'],
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/HTTP/Client.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | class CouchDBHelper extends Helper
20 | {
21 | protected $couchDBClient;
22 |
23 | /**
24 | * @var DocumentManager
25 | */
26 | protected $dm;
27 |
28 | /**
29 | * Constructor.
30 | *
31 | * @param CouchDBClient $couchDBClient
32 | * @param DocumentManager $dm
33 | */
34 | public function __construct(CouchDBClient $couchDBClient = null, DocumentManager $dm = null)
35 | {
36 | if (!$couchDBClient && $dm) {
37 | $couchDBClient = $dm->getCouchDBClient();
38 | }
39 |
40 | $this->couchDBClient = $couchDBClient;
41 | $this->dm = $dm;
42 | }
43 |
44 | /**
45 | * Retrieves Doctrine ODM CouchDB Manager.
46 | *
47 | * @return \Doctrine\ODM\CouchDB\DocumentManager
48 | */
49 | public function getDocumentManager()
50 | {
51 | return $this->dm;
52 | }
53 |
54 | /**
55 | * @return CouchDBClient
56 | */
57 | public function getCouchDBClient()
58 | {
59 | return $this->couchDBClient;
60 | }
61 |
62 | /**
63 | * @see Helper
64 | */
65 | public function getName()
66 | {
67 | return 'couchdb';
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/MangoClient.php:
--------------------------------------------------------------------------------
1 | databaseName.'/_index';
18 |
19 | $params = ['index'=>['fields'=>$fields]];
20 |
21 | if ($ddoc) {
22 | $params['ddoc'] = $ddoc;
23 | }
24 | if ($name) {
25 | $params['name'] = $name;
26 | }
27 |
28 | return $this->httpClient->request('POST', $documentPath, json_encode($params));
29 | }
30 |
31 | /**
32 | * Delete a mango query index and return the HTTP response.
33 | *
34 | * @param string $ddoc - design document name
35 | * @param string $name - view name
36 | */
37 | public function deleteMangoIndex($ddoc, $name)
38 | {
39 | $documentPath = '/'.$this->databaseName.'/_index/_design/'.$ddoc.'/json/'.$name;
40 | $response = $this->httpClient->request('DELETE', $documentPath);
41 |
42 | return (isset($response->body['ok'])) ? true : false;
43 | }
44 |
45 | /**
46 | * Find documents using Mango Query.
47 | *
48 | * @param MangoQuery $query
49 | *
50 | * @return HTTP\Response
51 | */
52 | public function find(MangoQuery $query)
53 | {
54 | $documentPath = '/'.$this->databaseName.'/_find';
55 | return $this->httpClient->request('POST', $documentPath, json_encode($query));
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/Tools/Migrations/AbstractMigration.php:
--------------------------------------------------------------------------------
1 | client = $client;
16 | }
17 |
18 | /**
19 | * Execute migration by iterating over all documents in batches of 100.
20 | *
21 | * @throws \RuntimeException
22 | *
23 | * @return void
24 | */
25 | public function execute()
26 | {
27 | $response = $this->client->allDocs(100);
28 | $lastKey = null;
29 |
30 | do {
31 | if ($response->status !== 200) {
32 | throw new \RuntimeException('Error while migrating at offset '.$offset);
33 | }
34 |
35 | $bulkUpdater = $this->client->createBulkUpdater();
36 | foreach ($response->body['rows'] as $row) {
37 | $doc = $this->migrate($row['doc']);
38 | if ($doc) {
39 | $bulkUpdater->updateDocument($doc);
40 | }
41 | $lastKey = $row['key'];
42 | }
43 |
44 | $bulkUpdater->execute();
45 | $response = $this->client->allDocs(100, $lastKey);
46 | } while (count($response->body['rows']) > 1);
47 | }
48 |
49 | /**
50 | * Return an array of to migrate to document data or null if this document should not be migrated.
51 | *
52 | * @param array $docData
53 | *
54 | * @return array|bool|null $docData
55 | */
56 | abstract protected function migrate(array $docData);
57 | }
58 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/CouchDBException.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class CouchDBException extends \Exception
16 | {
17 | public static function unknownDocumentNamespace($documentNamespaceAlias)
18 | {
19 | return new self("Unknown Document namespace alias '$documentNamespaceAlias'.");
20 | }
21 |
22 | public static function unregisteredDesignDocument($designDocumentName)
23 | {
24 | return new self("No design document with name '".$designDocumentName."' was registered with the DocumentManager.");
25 | }
26 |
27 | public static function invalidAttachment($className, $id, $filename)
28 | {
29 | return new self('Trying to save invalid attachment with filename '.$filename.' in document '.$className.' with id '.$id);
30 | }
31 |
32 | public static function detachedDocumentFound($className, $id, $assocName)
33 | {
34 | return new self('Found a detached or new document at property '.
35 | $className.'::'.$assocName.' of document with ID '.$id.', '.
36 | 'but the assocation is not marked as cascade persist.');
37 | }
38 |
39 | public static function persistRemovedDocument()
40 | {
41 | return new self('Trying to persist document that is scheduled for removal.');
42 | }
43 |
44 | public static function luceneNotConfigured()
45 | {
46 | return new self('CouchDB Lucene is not configured. You have to configure the handler name to enable support for Lucene Queries.');
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/Utils/BulkUpdater.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | class BulkUpdater
18 | {
19 | private $data = ['docs' => []];
20 |
21 | private $requestHeaders = [];
22 |
23 | private $httpClient;
24 |
25 | private $databaseName;
26 |
27 | public function __construct(Client $httpClient, $databaseName)
28 | {
29 | $this->httpClient = $httpClient;
30 | $this->databaseName = $databaseName;
31 | }
32 |
33 | public function updateDocument($data)
34 | {
35 | $this->data['docs'][] = $data;
36 | }
37 |
38 | public function updateDocuments(array $docs)
39 | {
40 | foreach ($docs as $doc) {
41 | $this->data['docs'][] = (is_array($doc) ? $doc : json_decode($doc, true));
42 | }
43 | }
44 |
45 | public function deleteDocument($id, $rev)
46 | {
47 | $this->data['docs'][] = ['_id' => $id, '_rev' => $rev, '_deleted' => true];
48 | }
49 |
50 | public function setNewEdits($newEdits)
51 | {
52 | $this->data['new_edits'] = (bool) $newEdits;
53 | }
54 |
55 | public function setFullCommitHeader($commit)
56 | {
57 | $this->requestHeaders['X-Couch-Full-Commit'] = (bool) $commit;
58 | }
59 |
60 | public function execute()
61 | {
62 | return $this->httpClient->request('POST', $this->getPath(), json_encode($this->data), false, $this->requestHeaders);
63 | }
64 |
65 | public function getPath()
66 | {
67 | return '/'.$this->databaseName.'/_bulk_docs';
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/tests/Doctrine/Tests/CouchDB/CouchDBFunctionalTestCase.php:
--------------------------------------------------------------------------------
1 | createCouchDBClient()->deleteDatabase($this->getTestDatabase());
17 | }
18 |
19 | /**
20 | * @return \Doctrine\CouchDB\HTTP\Client
21 | */
22 | public function getHttpClient()
23 | {
24 | if ($this->httpClient === null) {
25 | if (isset($GLOBALS['DOCTRINE_COUCHDB_CLIENT'])) {
26 | $this->httpClient = new $GLOBALS['DOCTRINE_COUCHDB_CLIENT']();
27 | } else {
28 | $this->httpClient = new SocketClient();
29 | }
30 | }
31 |
32 | return $this->httpClient;
33 | }
34 |
35 | public function getTestDatabase()
36 | {
37 | return TestUtil::getTestDatabase();
38 | }
39 |
40 | public function getBulkTestDatabase()
41 | {
42 | return TestUtil::getBulkTestDatabase();
43 | }
44 |
45 | public function createCouchDBClient()
46 | {
47 | return new CouchDBClient($this->getHttpClient(), $this->getTestDatabase());
48 | }
49 |
50 | public function createMangoClient(){
51 | return new MangoClient($this->getHttpClient(), $this->getTestDatabase());
52 | }
53 |
54 | public function createCouchDBClientForBulkTest()
55 | {
56 | return new CouchDBClient($this->getHttpClient(), $this->getBulkTestDatabase());
57 | }
58 |
59 | public function createMangoClientForBulkTest()
60 | {
61 | return new MangoClient($this->getHttpClient(), $this->getBulkTestDatabase());
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/Tools/Console/Command/ReplicationCancelCommand.php:
--------------------------------------------------------------------------------
1 | setName('couchdb:replication:cancel')
16 | ->setDescription('Cancel replication from a given source to target.')
17 | ->setDefinition([
18 | new InputArgument('source', InputArgument::REQUIRED, 'Source Database', null),
19 | new InputArgument('target', InputArgument::REQUIRED, 'Target Database', null),
20 | new InputOption('continuous', 'c', InputOption::VALUE_NONE, 'Enable continuous replication', null),
21 | ])->setHelp(<<<'EOT'
22 | With this command you cancel the replication between a given source and target.
23 | All the options to POST /db/_replicate are available. Example usage:
24 |
25 | doctrine-couchdb couchdb:replication:cancel example-source-db example-target-db
26 | doctrine-couchdb couchdb:replication:cancel example-source-db http://example.com:5984/example-target-db
27 |
28 | EOT
29 | );
30 | }
31 |
32 | protected function execute(InputInterface $input, OutputInterface $output)
33 | {
34 | $couchClient = $this->getHelper('couchdb')->getCouchDBClient();
35 | /* @var $couchClient \Doctrine\CouchDB\CouchDBClient */
36 | $data = $couchClient->replicate(
37 | $input->getArgument('source'),
38 | $input->getArgument('target'),
39 | true,
40 | $input->getOption('continuous') ? true : false
41 | );
42 |
43 | $output->writeln('Replication canceled.');
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/View/FolderDesignDocument.php:
--------------------------------------------------------------------------------
1 | folderPath = realpath($folderPath);
20 | }
21 |
22 | public function getData()
23 | {
24 | if ($this->data === null) {
25 | $rdi = new \RecursiveDirectoryIterator($this->folderPath, \FilesystemIterator::CURRENT_AS_FILEINFO);
26 | $ri = new \RecursiveIteratorIterator($rdi, \RecursiveIteratorIterator::LEAVES_ONLY);
27 |
28 | $this->data = [];
29 | foreach ($ri as $path) {
30 | $fileData = $this->getFileData($path);
31 | if ($fileData !== null) {
32 | $parts = explode(DIRECTORY_SEPARATOR, ltrim(str_replace($this->folderPath, '', $fileData['key']), DIRECTORY_SEPARATOR));
33 |
34 | if (count($parts) == 3) {
35 | $this->data[$parts[0]][$parts[1]][$parts[2]] = $fileData['data'];
36 | } elseif (count($parts) == 2) {
37 | $this->data[$parts[0]][$parts[1]] = $fileData['data'];
38 | } elseif (count($parts) == 1) {
39 | $this->data[$parts[0]] = $fileData['data'];
40 | }
41 | }
42 | }
43 |
44 | $this->data['language'] = 'javascript';
45 | }
46 |
47 | return $this->data;
48 | }
49 |
50 | private function getFileData($path)
51 | {
52 | $result = null;
53 | if (substr($path, -3) === '.js') {
54 | $result = ['key' => str_replace('.js', '', $path),
55 | 'data'=> file_get_contents($path), ];
56 | } elseif (substr($path, -5) === '.json') {
57 | $result = ['key' => str_replace('.json', '', $path),
58 | 'data'=> json_decode(file_get_contents($path), true), ];
59 | }
60 |
61 | return $result;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/Tools/Console/Command/ReplicationStartCommand.php:
--------------------------------------------------------------------------------
1 | setName('couchdb:replication:start')
16 | ->setDescription('Start replication from a given source to target.')
17 | ->setDefinition([
18 | new InputArgument('source', InputArgument::REQUIRED, 'Source Database', null),
19 | new InputArgument('target', InputArgument::REQUIRED, 'Target Database', null),
20 | new InputOption('continuous', 'c', InputOption::VALUE_NONE, 'Enable continuous replication', null),
21 | new InputOption('proxy', 'p', InputOption::VALUE_REQUIRED, 'Proxy server to replicate through', null),
22 | new InputOption('id', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Ids for named replication', null),
23 | new InputOption('filter', 'f', InputOption::VALUE_REQUIRED, 'Replication-Filter Document', null),
24 | ])->setHelp(<<<'EOT'
25 | With this command you start the replication between a given source and target.
26 | All the options to POST /db/_replicate are available. Example usage:
27 |
28 | doctrine-couchdb couchdb:replication:start example-source-db example-target-db
29 | doctrine-couchdb couchdb:replication:start example-source-db http://example.com:5984/example-target-db
30 |
31 | EOT
32 | );
33 | }
34 |
35 | protected function execute(InputInterface $input, OutputInterface $output)
36 | {
37 | $couchClient = $this->getHelper('couchdb')->getCouchDBClient();
38 | /* @var $couchClient \Doctrine\CouchDB\CouchDBClient */
39 | $data = $couchClient->replicate(
40 | $input->getArgument('source'),
41 | $input->getArgument('target'), null,
42 | $input->getOption('continuous') ? true : false,
43 | $input->getOption('filter') ?: null,
44 | $input->getOption('id') ?: null,
45 | $input->getOption('proxy') ?: null
46 | );
47 |
48 | $output->writeln('Replication started.');
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/HTTP/HTTPException.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class HTTPException extends \Doctrine\CouchDB\CouchDBException
16 | {
17 | /**
18 | * @param string $ip
19 | * @param int $port
20 | * @param string $errstr
21 | * @param int $errno
22 | *
23 | * @return \Doctrine\CouchDB\HTTP\HTTPException
24 | */
25 | public static function connectionFailure($ip, $port, $errstr, $errno)
26 | {
27 | return new self(sprintf(
28 | "Could not connect to server at %s:%d: '%d: %s'",
29 | $ip,
30 | $port,
31 | $errno,
32 | $errstr
33 | ), $errno);
34 | }
35 |
36 | /**
37 | * @param string $ip
38 | * @param int $port
39 | * @param string $errstr
40 | * @param int $errno
41 | *
42 | * @return \Doctrine\CouchDB\HTTP\HTTPException
43 | */
44 | public static function readFailure($ip, $port, $errstr, $errno)
45 | {
46 | return new static(sprintf(
47 | "Could read from server at %s:%d: '%d: %s'",
48 | $ip,
49 | $port,
50 | $errno,
51 | $errstr
52 | ), $errno);
53 | }
54 |
55 | /**
56 | * @param string $path
57 | * @param Response $response
58 | *
59 | * @return \Doctrine\CouchDB\HTTP\HTTPException
60 | */
61 | public static function fromResponse($path, Response $response)
62 | {
63 | $response = self::fixCloudantBulkCustomError($response);
64 |
65 | if (!isset($response->body['error'])) {
66 | $response->body['error'] = '';
67 | }
68 |
69 | if (!isset($response->body['reason'])) {
70 | $response->body['reason'] = '';
71 | }
72 |
73 | return new self(
74 | 'HTTP Error with status '.$response->status.' occurred while '
75 | .'requesting '.$path.'. Error: '.$response->body['error']
76 | .' '.$response->body['reason'],
77 | $response->status);
78 | }
79 |
80 | private static function fixCloudantBulkCustomError($response)
81 | {
82 | if (isset($response->body[0]['error']) && isset($response->body[0]['reason'])) {
83 | $response->body['error'] = $response->body[0]['error'];
84 | $response->body['reason'] = $response->body[0]['reason'];
85 | }
86 |
87 | return $response;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/HTTP/LoggingClient.php:
--------------------------------------------------------------------------------
1 | client = $client;
42 | }
43 |
44 | public function request($method, $path, $data = null, $raw = false, array $headers = [])
45 | {
46 | $start = microtime(true);
47 |
48 | $response = $this->client->request($method, $path, $data, $raw, $headers);
49 |
50 | $duration = microtime(true) - $start;
51 | $this->requests[] = [
52 | 'duration' => $duration,
53 | 'method' => $method,
54 | 'path' => rawurldecode($path),
55 | 'request' => $data,
56 | 'request_size' => strlen($data),
57 | 'response_status' => $response->status,
58 | 'response' => $response->body,
59 | 'response_headers' => $response->headers,
60 | ];
61 | $this->totalDuration += $duration;
62 |
63 | return $response;
64 | }
65 |
66 | public function getConnection(
67 | $method,
68 | $path,
69 | $data = null,
70 | array $headers = []
71 | ) {
72 | $start = microtime(true);
73 |
74 | $response = $this->client->getConnection(
75 | $method,
76 | $path,
77 | $data,
78 | $headers
79 | );
80 |
81 | $duration = microtime(true) - $start;
82 | $this->requests[] = [
83 | 'duration' => $duration,
84 | 'method' => $method,
85 | 'path' => rawurldecode($path),
86 | 'request' => $data,
87 | 'request_size' => strlen($data),
88 | 'response_status' => $response->status,
89 | 'response' => $response->body,
90 | 'response_headers' => $response->headers,
91 | ];
92 | $this->totalDuration += $duration;
93 |
94 | return $response;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/Mango/MangoQuery.php:
--------------------------------------------------------------------------------
1 | selector = $selector;
19 | $this->options = $options;
20 |
21 | if (isset($this->options['fields'])) {
22 | $this->fields = $this->options['fields'];
23 | }
24 |
25 | if (isset($this->options['sort'])) {
26 | $this->sort = $this->options['sort'];
27 | }
28 |
29 | if (isset($this->options['skip'])) {
30 | $this->skip = $this->options['skip'];
31 | }
32 |
33 | if (isset($this->options['limit'])) {
34 | $this->limit = $this->options['limit'];
35 | }
36 |
37 | if (isset($this->options['use_index'])) {
38 | $this->use_index = $this->options['use_index'];
39 | }
40 |
41 | }
42 |
43 | public function where(array $selector){
44 | return $this->selector($selector);
45 | }
46 |
47 | public function selector(array $selector = null){
48 | if($selector !== null){
49 | $this->selector = $selector;
50 | return $this;
51 | }else{
52 | return $this->selector;
53 | }
54 | }
55 |
56 | public function select(array $fields){
57 | return $this->fields($fields);
58 | }
59 |
60 | public function fields(array $fields = null){
61 | if($fields !== null){
62 | $this->fields = $fields;
63 | return $this;
64 | }else{
65 | return $this->fields;
66 | }
67 | }
68 |
69 | public function sort(array $sort = null){
70 | if($sort !== null){
71 | $this->sort = $sort;
72 | return $this;
73 | }else{
74 | return $this->sort;
75 | }
76 | }
77 |
78 | public function skip($skip = null){
79 | if($skip !== null){
80 | $this->skip = $skip;
81 | return $this;
82 | }else{
83 | return $this->skip;
84 | }
85 | }
86 |
87 | public function use_index(array $use_index = null){
88 | if($use_index !== null){
89 | $this->use_index = $use_index;
90 | return $this;
91 | }else{
92 | return $this->use_index;
93 | }
94 | }
95 |
96 | public function limit($limit = null){
97 | if($limit !== null){
98 | $this->limit = $limit;
99 | return $this;
100 | }else{
101 | return $this->limit;
102 | }
103 | }
104 |
105 | public function asArray(){
106 |
107 | $params = array();
108 | $params['selector'] = ($this->selector) ? $this->selector : new \StdClass();
109 |
110 | if ($this->fields) {
111 | $params['fields'] = $this->fields;
112 | }
113 |
114 | if ($this->sort) {
115 | $params['sort'] = $this->sort;
116 | }
117 |
118 | if ($this->skip) {
119 | $params['skip'] = $this->skip;
120 | }
121 |
122 | if ($this->limit) {
123 | $params['limit'] = $this->limit;
124 | }
125 |
126 | if ($this->use_index) {
127 | $params['use_index'] = $this->use_index;
128 | }
129 |
130 | return $params;
131 | }
132 |
133 | public function jsonSerialize() {
134 | return $this->asArray();
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/HTTP/AbstractHTTPClient.php:
--------------------------------------------------------------------------------
1 |
17 | */
18 | abstract class AbstractHTTPClient implements Client
19 | {
20 | /**
21 | * CouchDB connection options.
22 | *
23 | * @var array
24 | */
25 | protected $options = [
26 | 'host' => 'localhost',
27 | 'port' => 5984,
28 | 'ip' => '127.0.0.1',
29 | 'ssl' => false,
30 | 'timeout' => 10,
31 | 'keep-alive' => true,
32 | 'username' => null,
33 | 'password' => null,
34 | 'path' => null,
35 | 'headers' => [],
36 | ];
37 |
38 | /**
39 | * Construct a CouchDB connection.
40 | *
41 | * Construct a CouchDB connection from basic connection parameters for one
42 | * given database.
43 | *
44 | * @param string $host
45 | * @param int $port
46 | * @param string $username
47 | * @param string $password
48 | * @param string $ip
49 | * @param bool $ssl
50 | * @param string $path
51 | * @param int $timeout
52 | * @param array $headers
53 | *
54 | * @return \Doctrine\CouchDB\HTTP\AbstractHTTPClient
55 | */
56 | public function __construct($host = 'localhost', $port = 5984, $username = null, $password = null, $ip = null, $ssl = false, $path = null, $timeout = 10, array $headers = [])
57 | {
58 | $this->options['host'] = (string) $host;
59 | $this->options['port'] = (int) $port;
60 | $this->options['ssl'] = $ssl;
61 | $this->options['username'] = $username;
62 | $this->options['password'] = $password;
63 | $this->options['path'] = $path;
64 | $this->options['timeout'] = (float) $timeout;
65 | $this->options['headers'] = $headers;
66 |
67 | if ($ip === null) {
68 | $this->options['ip'] = gethostbyname($this->options['host']);
69 | } else {
70 | $this->options['ip'] = $ip;
71 | }
72 | }
73 |
74 | /**
75 | * Set option value.
76 | *
77 | * Set the value for an connection option. Throws an
78 | * InvalidArgumentException for unknown options.
79 | *
80 | * @param string $option
81 | * @param mixed $value
82 | *
83 | * @throws \InvalidArgumentException
84 | *
85 | * @return void
86 | */
87 | public function setOption($option, $value)
88 | {
89 | switch ($option) {
90 | case 'keep-alive':
91 | case 'ssl':
92 | $this->options[$option] = (bool) $value;
93 | break;
94 |
95 | case 'http-log':
96 | case 'headers':
97 | case 'password':
98 | case 'username':
99 | $this->options[$option] = $value;
100 | break;
101 |
102 | default:
103 | throw new \InvalidArgumentException("Unknown option $option.");
104 | }
105 | }
106 |
107 | /**
108 | * Get the connection options.
109 | *
110 | * @return array
111 | */
112 | public function getOptions()
113 | {
114 | return $this->options;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/View/LuceneQuery.php:
--------------------------------------------------------------------------------
1 | handlerName = $handlerName;
28 | }
29 |
30 | protected function getHttpQuery()
31 | {
32 | return sprintf(
33 | '/%s/local/%s/_design/%s/%s?%s',
34 | $this->handlerName,
35 | $this->databaseName,
36 | $this->designDocumentName,
37 | $this->viewName,
38 | http_build_query($this->params)
39 | );
40 | }
41 |
42 | public function setAnalyzer($analyzer)
43 | {
44 | $this->params['analyzer'] = $analyzer;
45 |
46 | return $this;
47 | }
48 |
49 | public function getAnalyzer()
50 | {
51 | return (isset($this->params['analyzer'])) ? $this->params['analyzer'] : null;
52 | }
53 |
54 | /**
55 | * Automatically fetch and include the document which emitted each view entry.
56 | *
57 | * @param bool $flag
58 | *
59 | * @return Query
60 | */
61 | public function setIncludeDocs($flag)
62 | {
63 | $this->params['include_docs'] = $flag;
64 |
65 | return $this;
66 | }
67 |
68 | public function getIncludeDocs()
69 | {
70 | return (isset($this->params['include_docs'])) ? $this->params['include_docs'] : null;
71 | }
72 |
73 | public function setLimit($limit)
74 | {
75 | $this->params['limit'] = $limit;
76 |
77 | return $this;
78 | }
79 |
80 | public function getLimit()
81 | {
82 | return (isset($this->params['limit'])) ? $this->params['limit'] : null;
83 | }
84 |
85 | public function setQuery($query)
86 | {
87 | $this->params['q'] = $query;
88 |
89 | return $this;
90 | }
91 |
92 | public function getQuery()
93 | {
94 | return isset($this->params['q']) ? $this->params['q'] : null;
95 | }
96 |
97 | public function setSkip($skip)
98 | {
99 | $this->params['skip'] = $skip;
100 |
101 | return $this;
102 | }
103 |
104 | public function setSort($sort)
105 | {
106 | $this->params['sort'] = $sort;
107 |
108 | return $this;
109 | }
110 |
111 | public function setStale($bool)
112 | {
113 | if ($bool) {
114 | $this->params['stale'] = 'ok';
115 | } else {
116 | unset($this->params['stale']);
117 | }
118 |
119 | return $this;
120 | }
121 |
122 | /**
123 | * @param \Doctrine\CouchDB\HTTP\Response $response
124 | *
125 | * @return LuceneResult
126 | */
127 | protected function createResult($response)
128 | {
129 | return new LuceneResult($response->body);
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/View/AbstractQuery.php:
--------------------------------------------------------------------------------
1 | client = $client;
50 | $this->databaseName = $databaseName;
51 | $this->designDocumentName = $designDocName;
52 | $this->viewName = $viewName;
53 | $this->doc = $doc;
54 | }
55 |
56 | /**
57 | * @param string $key
58 | *
59 | * @return mixed
60 | */
61 | public function getParameter($key)
62 | {
63 | if (isset($this->params[$key])) {
64 | return $this->params[$key];
65 | }
66 | }
67 |
68 | abstract protected function getHttpQuery();
69 |
70 | /**
71 | * Query the view with the current params.
72 | *
73 | * @return Result
74 | */
75 | public function execute()
76 | {
77 | return $this->createResult($this->doExecute());
78 | }
79 |
80 | protected function doExecute()
81 | {
82 | $path = $this->getHttpQuery();
83 | $method = 'GET';
84 | $data = null;
85 |
86 | if ($this->getParameter('keys') !== null) {
87 | $method = 'POST';
88 | $data = json_encode(['keys' => $this->getParameter('keys')]);
89 | }
90 |
91 | $response = $this->client->request($method, $path, $data);
92 |
93 | if ($response instanceof ErrorResponse && $this->doc) {
94 | $this->createDesignDocument();
95 | $response = $this->client->request($method, $path, $data);
96 | }
97 |
98 | if ($response->status >= 400) {
99 | throw HTTPException::fromResponse($path, $response);
100 | }
101 |
102 | return $response;
103 | }
104 |
105 | /**
106 | * @param $response
107 | *
108 | * @return Result
109 | */
110 | abstract protected function createResult($response);
111 |
112 | /**
113 | * Create non existing view.
114 | *
115 | * @throws \Doctrine\CouchDB\JsonDecodeException
116 | * @throws \Exception
117 | *
118 | * @return void
119 | */
120 | public function createDesignDocument()
121 | {
122 | if (!$this->doc) {
123 | throw new \Exception('No DesignDocument Class is connected to this view query, cannot create the design document with its corresponding view automatically!');
124 | }
125 |
126 | $data = $this->doc->getData();
127 | if ($data === null) {
128 | throw \Doctrine\CouchDB\JsonDecodeException::fromLastJsonError();
129 | }
130 | $data['_id'] = '_design/'.$this->designDocumentName;
131 |
132 | $response = $this->client->request(
133 | 'PUT',
134 | sprintf(
135 | '/%s/_design/%s',
136 | $this->databaseName,
137 | $this->designDocumentName
138 | ),
139 | json_encode($data)
140 | );
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/tests/Doctrine/Tests/CouchDB/CouchDBClientTest.php:
--------------------------------------------------------------------------------
1 | assertEquals("\xEF\xBF\xB0", CouchDBClient::COLLATION_END);
13 | }
14 |
15 | public function testCreateClient()
16 | {
17 | $client = CouchDBClient::create(['dbname' => 'test']);
18 | $this->assertEquals('test', $client->getDatabase());
19 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Client', $client->getHttpClient());
20 |
21 | $httpClient = new SocketClient();
22 | $client->setHttpClient($httpClient);
23 | $this->assertEquals($httpClient, $client->getHttpClient());
24 | }
25 |
26 | public function testCreateClientFromUrl()
27 | {
28 | $client = CouchDBClient::create(['url' => 'https://foo:bar@localhost:5555/baz']);
29 |
30 | $this->assertEquals('baz', $client->getDatabase());
31 | $this->assertEquals(
32 | [
33 | 'host' => 'localhost',
34 | 'port' => 5555,
35 | 'ip' => '127.0.0.1',
36 | 'username' => 'foo',
37 | 'password' => 'bar',
38 | 'ssl' => true,
39 | 'timeout' => 10,
40 | 'keep-alive' => true,
41 | 'path' => null,
42 | 'headers' => [],
43 | ],
44 | $client->getHttpClient()->getOptions()
45 | );
46 | }
47 |
48 | public function testCreateClientFromUrlWithPath()
49 | {
50 | $client = CouchDBClient::create(['url' => 'https://foo:bar@localhost:5555/baz/qux/norf']);
51 |
52 | $this->assertEquals('norf', $client->getDatabase());
53 | $this->assertEquals(
54 | [
55 | 'host' => 'localhost',
56 | 'port' => 5555,
57 | 'ip' => '127.0.0.1',
58 | 'username' => 'foo',
59 | 'password' => 'bar',
60 | 'ssl' => true,
61 | 'timeout' => 10,
62 | 'keep-alive' => true,
63 | 'path' => 'baz/qux',
64 | 'headers' => [],
65 | ],
66 | $client->getHttpClient()->getOptions()
67 | );
68 | }
69 |
70 | public function testCreateClientWithDefaultHeaders()
71 | {
72 | $client = CouchDBClient::create(['dbname' => 'test', 'headers' => ['X-Test' => 'test']]);
73 | $http_client = $client->getHttpClient();
74 | $connection_options = $http_client->getOptions();
75 | $this->assertSame(['X-Test' => 'test'], $connection_options['headers']);
76 |
77 | $http_client->setOption('headers', ['X-Test-New' => 'new']);
78 | $connection_options = $http_client->getOptions();
79 | $this->assertSame(['X-Test-New' => 'new'], $connection_options['headers']);
80 | }
81 |
82 | public function testCreateClientWithLogging()
83 | {
84 | $client = CouchDBClient::create(['dbname' => 'test', 'logging' => true]);
85 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\LoggingClient', $client->getHttpClient());
86 | }
87 |
88 | /**
89 | * @expectedException \InvalidArgumentException
90 | * @expectedExceptionMessage 'dbname' is a required option to create a CouchDBClient
91 | */
92 | public function testCreateClientDBNameException()
93 | {
94 | CouchDBClient::create([]);
95 | }
96 |
97 | /**
98 | * @expectedException \InvalidArgumentException
99 | * @expectedExceptionMessage There is no client implementation registered for foo, valid options are: socket, stream
100 | */
101 | public function testCreateClientMissingClientException()
102 | {
103 | CouchDBClient::create(['dbname' => 'test', 'type' => 'foo']);
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/tests/Doctrine/Tests/CouchDB/Functional/BulkUpdaterTest.php:
--------------------------------------------------------------------------------
1 | couchClient = $this->createCouchDBClientForBulkTest();
23 | $this->couchClient->createDatabase($this->getBulkTestDatabase());
24 | $this->bulkUpdater = $this->couchClient->createBulkUpdater();
25 | }
26 |
27 | public function testGetPath()
28 | {
29 | $this->assertEquals(
30 | '/'.$this->getBulkTestDatabase().'/_bulk_docs',
31 | $this->bulkUpdater->getpath()
32 | );
33 | }
34 |
35 | /**
36 | * @depends testGetPath
37 | */
38 | public function testExecute()
39 | {
40 | $response = $this->bulkUpdater->execute();
41 | $this->assertEquals(201, $response->status);
42 | $this->assertEquals([], $response->body);
43 | }
44 |
45 | /**
46 | * @depends testExecute
47 | */
48 | public function testSetNewEdits()
49 | {
50 | $this->bulkUpdater->setNewEdits(false);
51 | $doc = ['_id' => 'test1', 'foo' => 'bar', '_rev' => '10-gsoc'];
52 | $this->bulkUpdater->updateDocument($doc);
53 | $response = $this->bulkUpdater->execute();
54 | $response = $this->couchClient->findDocument('test1');
55 | // _rev remains same.
56 | $this->assertEquals($doc, $response->body);
57 | }
58 |
59 | /**
60 | * @depends testExecute
61 | */
62 | public function testUpdateDocument()
63 | {
64 | $docs['test1'] = ['_id' => 'test1', 'foo' => 'bar'];
65 | $docs['test2'] = ['_id' => 'test2', 'bar' => 'baz'];
66 | $this->bulkUpdater->updateDocument($docs['test1']);
67 | $this->bulkUpdater->updateDocument($docs['test2']);
68 | $response = $this->bulkUpdater->execute();
69 |
70 | // Insert the rev values.
71 | foreach ($response->body as $res) {
72 | $docs[$res['id']]['_rev'] = $res['rev'];
73 | }
74 |
75 | $response = $this->couchClient->findDocument('test1');
76 | $this->assertEquals($docs['test1'], $response->body);
77 | $response = $this->couchClient->findDocument('test2');
78 | $this->assertEquals($docs['test2'], $response->body);
79 | }
80 |
81 | /**
82 | * @depends testExecute
83 | */
84 | public function testUpdateDocuments()
85 | {
86 | $docs[] = ['_id' => 'test1', 'foo' => 'bar'];
87 | $docs[] = '{"_id": "test2","baz": "foo"}';
88 |
89 | $this->bulkUpdater->updateDocuments($docs);
90 | $response = $this->bulkUpdater->execute();
91 |
92 | // Insert the rev values.
93 | foreach ($response->body as $res) {
94 | $id = $res['id'];
95 | if ($id == 'test1') {
96 | $docs[0]['_rev'] = $res['rev'];
97 | } elseif ($id == 'test2') {
98 | $docs[1] = substr($docs[1], 0, strlen($docs[1]) - 1).',"_rev": "'.$res['rev'].'"}';
99 | }
100 | }
101 |
102 | $response = $this->couchClient->findDocument('test1');
103 | $this->assertEquals($docs[0], $response->body);
104 | $response = $this->couchClient->findDocument('test2');
105 | $this->assertEquals(json_decode($docs[1], true), $response->body);
106 | }
107 |
108 | /**
109 | * @depends testExecute
110 | */
111 | public function testDeleteDocument()
112 | {
113 | $doc = ['_id' => 'test1', 'foo' => 'bar'];
114 | $this->bulkUpdater->updateDocument($doc);
115 | $response = $this->bulkUpdater->execute();
116 | $rev = $response->body[0]['rev'];
117 |
118 | $bulkUpdater2 = $this->couchClient->createBulkUpdater();
119 | $bulkUpdater2->deleteDocument('test1', $rev);
120 | $response = $bulkUpdater2->execute();
121 | $response = $this->couchClient->findDocument('test1');
122 | $this->assertEquals(404, $response->status);
123 | }
124 |
125 | public function tearDown()
126 | {
127 | $this->couchClient->deleteDatabase($this->getBulkTestDatabase());
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/tests/Doctrine/Tests/CouchDB/Functional/MangoQueryTest.php:
--------------------------------------------------------------------------------
1 | ['$gt'=>null]];
13 | $query = new MangoQuery($selector);
14 | $query = $query->asArray();
15 | $this->assertEquals($selector,$query['selector']);
16 | }
17 |
18 | public function testEmptySelector(){
19 | $query = new MangoQuery([]);
20 | $query = $query->asArray();
21 | $this->assertInstanceOf('StdClass',$query['selector']);
22 | }
23 |
24 | public function testSelectorMethod(){
25 | $query = new MangoQuery([]);
26 | $selector = ['id'=>['$gt'=>null]];
27 | $query->selector($selector);
28 | $this->assertTrue(array_key_exists('selector',$query->asArray()));
29 | $this->assertEquals($selector,$query->selector());
30 | }
31 |
32 | public function testSetEmptySelector(){
33 | $query = new MangoQuery([]);
34 | $query->selector([]);
35 | $query = $query->asArray();
36 | $this->assertInstanceOf('StdClass',$query['selector']);
37 | }
38 |
39 | public function testLimitOption(){
40 | $query = new MangoQuery([],['limit'=>1]);
41 | $query = $query->asArray();
42 | $this->assertTrue(array_key_exists('limit',$query));
43 | $this->assertEquals(1,$query['limit']);
44 | }
45 |
46 | public function testLimitMethod(){
47 | $query = new MangoQuery([]);
48 | $query->limit(1);
49 | $this->assertTrue(array_key_exists('limit',$query->asArray()));
50 | $this->assertEquals(1,$query->limit());
51 | }
52 |
53 | public function testSkipOption(){
54 | $query = new MangoQuery([],['skip'=>1]);
55 | $query = $query->asArray();
56 | $this->assertTrue(array_key_exists('skip',$query));
57 | $this->assertEquals(1,$query['skip']);
58 | }
59 |
60 | public function testSkipMethod(){
61 | $query = new MangoQuery([]);
62 | $query->skip(1);
63 | $this->assertTrue(array_key_exists('skip',$query->asArray()));
64 | $this->assertEquals(1,$query->skip());
65 | }
66 |
67 | public function testFieldsOption(){
68 | $query = new MangoQuery([],['fields'=>['_id','_rev']]);
69 | $query = $query->asArray();
70 | $this->assertTrue(array_key_exists('fields',$query));
71 | $this->assertEquals(['_id','_rev'],$query['fields']);
72 | }
73 |
74 | public function testFieldsMethod(){
75 | $query = new MangoQuery([]);
76 | $query->fields(['_id','_rev']);
77 | $this->assertTrue(array_key_exists('fields',$query->asArray()));
78 | $this->assertEquals(['_id','_rev'],$query->fields());
79 | }
80 |
81 | public function testSortOption(){
82 | $query = new MangoQuery([],['sort'=>[['name'=>'desc']]]);
83 | $query = $query->asArray();
84 | $this->assertTrue(array_key_exists('sort',$query));
85 | $this->assertEquals([['name'=>'desc']],$query['sort']);
86 | }
87 |
88 | public function testSortMethod(){
89 | $query = new MangoQuery([]);
90 | $query->sort([['name'=>'desc']]);
91 | $this->assertTrue(array_key_exists('sort',$query->asArray()));
92 | $this->assertEquals([['name'=>'desc']],$query->sort());
93 | }
94 |
95 | public function testUseIndexOption(){
96 | $query = new MangoQuery([],['use_index'=>['document','index']]);
97 | $query = $query->asArray();
98 | $this->assertTrue(array_key_exists('use_index',$query));
99 | $this->assertEquals(['document','index'],$query['use_index']);
100 | }
101 |
102 | public function testUseIndexMethod(){
103 | $query = new MangoQuery([]);
104 | $query->use_index(['document','index']);
105 | $this->assertTrue(array_key_exists('use_index',$query->asArray()));
106 | $this->assertEquals(['document','index'],$query->use_index());
107 | }
108 |
109 | public function testJsonSerialize(){
110 | $query = new MangoQuery();
111 |
112 | $params = [
113 | 'selector'=>['_id'=>['$gt'=>null]],
114 | 'fields'=>['_id'],
115 | 'sort'=>[['name'=>'desc']],
116 | 'skip'=>1,
117 | 'limit'=>10,
118 | 'use_index'=>['design','document']
119 | ];
120 |
121 | $query->select($params['fields'])->where($params['selector'])->limit($params['limit'])->skip($params['skip'])->sort($params['sort'])->use_index($params['use_index']);
122 |
123 | $this->assertEquals(json_encode($params),json_encode($query));
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Doctrine CouchDB v2.x Client
2 |
3 | [](https://travis-ci.org/doctrine/couchdb-client)
4 | [](https://styleci.io/repos/90809440)
5 |
6 |
7 | Simple API that wraps around CouchDBs v2.x HTTP API.
8 |
9 | For CouchDB 1.x, please check our [release/1.0.0](https://github.com/doctrine/couchdb-client/tree/release/1.0.0) branch.
10 |
11 | ## Features
12 |
13 | * Create, Delete, List Databases
14 | * Create, Update, Delete Documents
15 | * Bulk API for Creating/Updating Documents
16 | * Find Documents by ID
17 | * Generate UUIDs
18 | * Design Documents
19 | * Query `_all_docs` view
20 | * Query Changes Feed
21 | * Query Views
22 | * Compaction Info and Triggering APIs
23 | * Replication API
24 | * Symfony Console Commands
25 | * Find Documents using Mango Query
26 |
27 | ## Installation
28 |
29 | With Composer:
30 |
31 | {
32 | "require": {
33 | "doctrine/couchdb": "@dev"
34 | }
35 | }
36 |
37 | ## Usage
38 |
39 | ### Basic Operations
40 |
41 | Covering the basic CRUD Operations for databases and documents:
42 |
43 | ```php
44 | 'doctrine_example'));
46 |
47 | // Create a database.
48 | $client->createDatabase($client->getDatabase());
49 |
50 | // Create a new document.
51 | list($id, $rev) = $client->postDocument(array('foo' => 'bar'));
52 |
53 | // Update a existing document. This will increment the revision.
54 | list($id, $rev) = $client->putDocument(array('foo' => 'baz'), $id, $rev);
55 |
56 | // Fetch single document by id.
57 | $doc = $client->findDocument($id);
58 |
59 | // Fetch multiple documents at once.
60 | $docs = $client->findDocuments(array($id));
61 |
62 | // Return all documents from database (_all_docs?include_docs=true).
63 | $allDocs = $client->allDocs();
64 |
65 | // Delete a single document.
66 | $client->deleteDocument($id, $rev);
67 |
68 | // Delete a database.
69 | $client->deleteDatabase($client->getDatabase());
70 |
71 | //Search documents using Mango Query CouchDB v2.x
72 |
73 | $selector = ['_id'=>['$gt'=>null]];
74 | $options = ['limit'=>1,'skip'=>1,'use_index'=>['_design/doc','index'],'sort'=>[['_id'=>'desc']]];
75 | $query = new \Doctrine\CouchDB\Mango\MangoQuery($selector,$options);
76 | $docs = $client->find($query);
77 |
78 | $query = new \Doctrine\CouchDB\Mango\MangoQuery();
79 | $query->select(['_id', 'name'])->where(['$and'=> [
80 | [
81 | 'name'=> [
82 | '$eq'=> 'Under the Dome',
83 | ],
84 | 'genres'=> [
85 | '$in'=> ['Drama','Comedy'],
86 | ],
87 | ],
88 | ])->sort([['_id'=>'desc']])->limit(1)->skip(1)->use_index(['_design/doc','index']);
89 | $docs = $client->find($query);
90 |
91 | ```
92 |
93 | ### Views
94 |
95 | A simple example demonstrating how to create views and query them:
96 |
97 | ```php
98 | 'javascript',
105 | 'views' => array(
106 | 'by_author' => array(
107 | 'map' => 'function(doc) {
108 | if(\'article\' == doc.type) {
109 | emit(doc.author, doc._id);
110 | }
111 | }',
112 | 'reduce' => '_count'
113 | ),
114 | ),
115 | );
116 | }
117 | }
118 |
119 | $client->createDesignDocument('articles', new ArticlesDesignDocument());
120 |
121 | // Fill database with some data.
122 | foreach (array('Alice', 'Bob', 'Bob') as $author) {
123 | $client->postDocument(array(
124 | 'type' => 'article',
125 | 'author' => $author,
126 | 'content' => 'Lorem ipsum'
127 | ));
128 | }
129 |
130 | // Query all articles.
131 | $query = $client->createViewQuery('articles', 'by_author');
132 | $query->setReduce(false);
133 | $query->setIncludeDocs(true);
134 | $result = $query->execute();
135 | foreach ($result as $row) {
136 | $doc = $row['doc'];
137 | echo 'Article by ', $doc['author'], ': ', $doc['content'], "\n";
138 | }
139 | // Article by Alice: Lorem ipsum
140 | // Article by Bob: Lorem ipsum
141 | // Article by Bob: Lorem ipsum
142 |
143 |
144 | // Query all articles written by bob.
145 | $query = $client->createViewQuery('articles', 'by_author');
146 | $query->setKey('Bob');
147 | // ...
148 |
149 |
150 | // Query the _count of articles each author has written.
151 | $query = $client->createViewQuery('articles', 'by_author');
152 | $query->setReduce(true);
153 | $query->setGroupLevel(1); // group_level=1 means grouping by author.
154 | $result = $query->execute();
155 | foreach ($result as $row) {
156 | echo 'Author ', $row['key'], ' has written ', $row['value'], ' articles', "\n";
157 | }
158 | // Author Alice has written 1 articles
159 | // Author Bob has written 2 articles
160 | ```
161 |
--------------------------------------------------------------------------------
/docs/en/index.rst:
--------------------------------------------------------------------------------
1 | Doctrine CouchDB Client
2 | =======================
3 |
4 | Doctrine CouchDB Client is a simple API that wraps around CouchDBs v2.x HTTP API.
5 |
6 | For CouchDB 1.x, please use our
7 | `release/1.0.0 `_
8 | branch.
9 |
10 | Features
11 | --------
12 |
13 | - Create, Delete, List Databases
14 | - Create, Update, Delete Documents
15 | - Bulk API for Creating/Updating Documents
16 | - Find Documents by ID
17 | - Generate UUIDs
18 | - Design Documents
19 | - Query ``_all_docs`` view
20 | - Query Changes Feed
21 | - Query Views
22 | - Compaction Info and Triggering APIs
23 | - Replication API
24 | - Symfony Console Commands
25 | - Find Documents using Mango Query
26 |
27 | Installation
28 | ------------
29 |
30 | With Composer:
31 |
32 | .. code-block:: console
33 |
34 | composer require doctrine/couchdb
35 |
36 | Usage
37 | -----
38 |
39 | Basic Operations
40 | ~~~~~~~~~~~~~~~~
41 |
42 | Covering the basic CRUD Operations for databases and documents:
43 |
44 | .. code-block:: php
45 |
46 | 'doctrine_example']);
55 |
56 | // Create a database.
57 | $client->createDatabase($client->getDatabase());
58 |
59 | // Create a new document.
60 | list($id, $rev) = $client->postDocument(['foo' => 'bar']);
61 |
62 | // Update a existing document. This will increment the revision.
63 | list($id, $rev) = $client->putDocument(['foo' => 'baz'], $id, $rev);
64 |
65 | // Fetch single document by id.
66 | $doc = $client->findDocument($id);
67 |
68 | // Fetch multiple documents at once.
69 | $docs = $client->findDocuments([$id]);
70 |
71 | // Return all documents from database (_all_docs?include_docs=true).
72 | $allDocs = $client->allDocs();
73 |
74 | // Delete a single document.
75 | $client->deleteDocument($id, $rev);
76 |
77 | // Delete a database.
78 | $client->deleteDatabase($client->getDatabase());
79 |
80 | // Search documents using Mango Query CouchDB v2.x
81 |
82 | $selector = ['_id' => ['$gt' => null]];
83 | $options = ['limit' => 1,'skip' => 1,'use_index' => ['_design/doc','index'],'sort' => [['_id' => 'desc']]];
84 | $query = new MangoQuery($selector, $options);
85 | $docs = $client->find($query);
86 |
87 | $query = new MangoQuery();
88 | $query->select(['_id', 'name'])->where(['$and'=> [
89 | [
90 | 'name'=> ['$eq' => 'Under the Dome'],
91 | 'genres'=> [
92 | '$in'=> ['Drama','Comedy'],
93 | ],
94 | ],
95 | ])
96 | ->sort([['_id' => 'desc']])
97 | ->limit(1)
98 | ->skip(1)
99 | ->use_index(['_design/doc', 'index']);
100 |
101 | $docs = $client->find($query);
102 |
103 | Views
104 | ~~~~~
105 |
106 | A simple example demonstrating how to create views and query them:
107 |
108 | .. code-block:: php
109 |
110 | class ArticlesDesignDocument implements DesignDocument
111 | {
112 | public function getData()
113 | {
114 | return [
115 | 'language' => 'javascript',
116 | 'views' => [
117 | 'by_author' => [
118 | 'map' => "function(doc) {
119 | if('article' == doc.type) {
120 | emit(doc.author, doc._id);
121 | }
122 | }",
123 | 'reduce' => '_count',
124 | ],
125 | ],
126 | ];
127 | }
128 | }
129 |
130 | $client->createDesignDocument('articles', new ArticlesDesignDocument());
131 |
132 | // Fill database with some data.
133 | foreach (['Alice', 'Bob', 'Bob'] as $author) {
134 | $client->postDocument([
135 | 'type' => 'article',
136 | 'author' => $author,
137 | 'content' => 'Lorem ipsum',
138 | ]);
139 | }
140 |
141 | // Query all articles.
142 | $query = $client->createViewQuery('articles', 'by_author');
143 | $query->setReduce(false);
144 | $query->setIncludeDocs(true);
145 | $result = $query->execute();
146 | foreach ($result as $row) {
147 | $doc = $row['doc'];
148 | echo 'Article by ', $doc['author'], ': ', $doc['content'], "\n";
149 | }
150 |
151 | // Article by Alice: Lorem ipsum
152 | // Article by Bob: Lorem ipsum
153 | // Article by Bob: Lorem ipsum
154 |
155 | // Query all articles written by bob.
156 | $query = $client->createViewQuery('articles', 'by_author');
157 | $query->setKey('Bob');
158 | // ...
159 |
160 |
161 | // Query the _count of articles each author has written.
162 | $query = $client->createViewQuery('articles', 'by_author');
163 | $query->setReduce(true);
164 | $query->setGroupLevel(1); // group_level=1 means grouping by author.
165 | $result = $query->execute();
166 | foreach ($result as $row) {
167 | echo 'Author ', $row['key'], ' has written ', $row['value'], ' articles', "\n";
168 | }
169 |
170 | // Author Alice has written 1 articles
171 | // Author Bob has written 2 articles
172 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/View/Query.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class Query extends AbstractQuery
16 | {
17 | /**
18 | * Only a subset of parameters in the Query String must be JSON encoded when transmitted.
19 | *
20 | * @param array
21 | */
22 | private static $encodeParams = ['key' => true, 'keys' => true, 'startkey' => true, 'endkey' => true];
23 |
24 | protected function createResult($response)
25 | {
26 | return new Result($response->body);
27 | }
28 |
29 | /**
30 | * Encode HTTP Query String for View correctly with the following rules in mind.
31 | *
32 | * 1. Params "key", "keys", "startkey" or "endkey" must be json encoded.
33 | * 2. Booleans must be converted to "true" or "false"
34 | *
35 | * @return string
36 | */
37 | protected function getHttpQuery()
38 | {
39 | $arguments = [];
40 |
41 | foreach ($this->params as $key => $value) {
42 | if (isset(self::$encodeParams[$key])) {
43 | $arguments[$key] = json_encode($value);
44 | } elseif (is_bool($value)) {
45 | $arguments[$key] = $value ? 'true' : 'false';
46 | } else {
47 | $arguments[$key] = $value;
48 | }
49 | }
50 |
51 | return sprintf(
52 | '/%s/_design/%s/_view/%s?%s',
53 | $this->databaseName,
54 | $this->designDocumentName,
55 | $this->viewName,
56 | http_build_query($arguments)
57 | );
58 | }
59 |
60 | /**
61 | * Find key in view.
62 | *
63 | * @param string|array $val
64 | *
65 | * @return Query
66 | */
67 | public function setKey($val)
68 | {
69 | $this->params['key'] = $val;
70 |
71 | return $this;
72 | }
73 |
74 | /**
75 | * Find keys in the view.
76 | *
77 | * @param array $values
78 | *
79 | * @return Query
80 | */
81 | public function setKeys(array $values)
82 | {
83 | $this->params['keys'] = $values;
84 |
85 | return $this;
86 | }
87 |
88 | /**
89 | * Set starting key to query view for.
90 | *
91 | * @param string $val
92 | *
93 | * @return Query
94 | */
95 | public function setStartKey($val)
96 | {
97 | $this->params['startkey'] = $val;
98 |
99 | return $this;
100 | }
101 |
102 | /**
103 | * Set ending key to query view for.
104 | *
105 | * @param string $val
106 | *
107 | * @return Query
108 | */
109 | public function setEndKey($val)
110 | {
111 | $this->params['endkey'] = $val;
112 |
113 | return $this;
114 | }
115 |
116 | /**
117 | * Document id to start with.
118 | *
119 | * @param string $val
120 | *
121 | * @return Query
122 | */
123 | public function setStartKeyDocId($val)
124 | {
125 | $this->params['startkey_docid'] = $val;
126 |
127 | return $this;
128 | }
129 |
130 | /**
131 | * Last document id to include in the output.
132 | *
133 | * @param string $val
134 | *
135 | * @return Query
136 | */
137 | public function setEndKeyDocId($val)
138 | {
139 | $this->params['endkey_docid'] = $val;
140 |
141 | return $this;
142 | }
143 |
144 | /**
145 | * Limit the number of documents in the output.
146 | *
147 | * @param int $val
148 | *
149 | * @return Query
150 | */
151 | public function setLimit($val)
152 | {
153 | $this->params['limit'] = $val;
154 |
155 | return $this;
156 | }
157 |
158 | /**
159 | * Skip n number of documents.
160 | *
161 | * @param int $val
162 | *
163 | * @return Query
164 | */
165 | public function setSkip($val)
166 | {
167 | $this->params['skip'] = $val;
168 |
169 | return $this;
170 | }
171 |
172 | /**
173 | * If stale=ok is set CouchDB will not refresh the view even if it is stalled.
174 | *
175 | * @param bool $flag
176 | *
177 | * @return Query
178 | */
179 | public function setStale($flag)
180 | {
181 | if (!is_bool($flag)) {
182 | $this->params['stale'] = $flag;
183 | } elseif ($flag === true) {
184 | $this->params['stale'] = 'ok';
185 | } else {
186 | unset($this->params['stale']);
187 | }
188 |
189 | return $this;
190 | }
191 |
192 | /**
193 | * reverse the output.
194 | *
195 | * @param bool $flag
196 | *
197 | * @return Query
198 | */
199 | public function setDescending($flag)
200 | {
201 | $this->params['descending'] = $flag;
202 |
203 | return $this;
204 | }
205 |
206 | /**
207 | * The group option controls whether the reduce function reduces to a set of distinct keys or to a single result row.
208 | *
209 | * @param bool $flag
210 | *
211 | * @return Query
212 | */
213 | public function setGroup($flag)
214 | {
215 | $this->params['group'] = $flag;
216 |
217 | return $this;
218 | }
219 |
220 | public function setGroupLevel($level)
221 | {
222 | $this->params['group_level'] = $level;
223 |
224 | return $this;
225 | }
226 |
227 | /**
228 | * Use the reduce function of the view. It defaults to true, if a reduce function is defined and to false otherwise.
229 | *
230 | * @param bool $flag
231 | *
232 | * @return Query
233 | */
234 | public function setReduce($flag)
235 | {
236 | $this->params['reduce'] = $flag;
237 |
238 | return $this;
239 | }
240 |
241 | /**
242 | * Controls whether the endkey is included in the result. It defaults to true.
243 | *
244 | * @param bool $flag
245 | *
246 | * @return Query
247 | */
248 | public function setInclusiveEnd($flag)
249 | {
250 | $this->params['inclusive_end'] = $flag;
251 |
252 | return $this;
253 | }
254 |
255 | /**
256 | * Automatically fetch and include the document which emitted each view entry.
257 | *
258 | * @param bool $flag
259 | *
260 | * @return Query
261 | */
262 | public function setIncludeDocs($flag)
263 | {
264 | $this->params['include_docs'] = $flag;
265 |
266 | return $this;
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/HTTP/StreamClient.php:
--------------------------------------------------------------------------------
1 |
17 | */
18 | class StreamClient extends AbstractHTTPClient
19 | {
20 | /**
21 | * Connection pointer for connections, once keep alive is working on the
22 | * CouchDb side.
23 | *
24 | * @var resource
25 | */
26 | protected $httpFilePointer;
27 |
28 | /**
29 | * Return the connection pointer after setting up the stream connection.
30 | * The returned resource can later be used to read data in chunks.
31 | *
32 | * @param string $method
33 | * @param string $path
34 | * @param string $data
35 | * @param array $headers
36 | *
37 | * @throws HTTPException
38 | *
39 | * @return resource
40 | */
41 | public function getConnection(
42 | $method,
43 | $path,
44 | $data = null,
45 | array $headers = []
46 | ) {
47 | $fullPath = $path;
48 | if ($this->options['path']) {
49 | $fullPath = '/'.$this->options['path'].$path;
50 | }
51 |
52 | $this->checkConnection($method, $fullPath, $data, $headers);
53 |
54 | return $this->httpFilePointer;
55 | }
56 |
57 | /**
58 | * Sets up the stream connection.
59 | *
60 | * @param $method
61 | * @param $path
62 | * @param $data
63 | * @param $headers
64 | *
65 | * @throws HTTPException
66 | */
67 | protected function checkConnection($method, $path, $data, $headers)
68 | {
69 | $basicAuth = '';
70 | if ($this->options['username']) {
71 | $basicAuth .= "{$this->options['username']}:{$this->options['password']}@";
72 | }
73 | if ($this->options['headers']) {
74 | $headers = array_merge($this->options['headers'], $headers);
75 | }
76 | if (!isset($headers['Content-Type'])) {
77 | $headers['Content-Type'] = 'application/json';
78 | }
79 | $stringHeader = '';
80 | if ($headers != null) {
81 | foreach ($headers as $key => $val) {
82 | $stringHeader .= $key.': '.$val."\r\n";
83 | }
84 | }
85 | if ($this->httpFilePointer == null) {
86 | // TODO SSL support?
87 | $host = $this->options['host'];
88 | if ($this->options['port'] != 80) {
89 | $host .= ":{$this->options['port']}";
90 | }
91 | $this->httpFilePointer = @fopen(
92 | 'http://'.$basicAuth.$host.$path,
93 | 'r',
94 | false,
95 | stream_context_create(
96 | [
97 | 'http' => [
98 | 'method' => $method,
99 | 'content' => $data,
100 | 'ignore_errors' => true,
101 | 'max_redirects' => 0,
102 | 'user_agent' => 'Doctrine CouchDB ODM $Revision$',
103 | 'timeout' => $this->options['timeout'],
104 | 'header' => $stringHeader,
105 | ],
106 | ]
107 | )
108 | );
109 | }
110 |
111 | // Check if connection has been established successfully.
112 | if ($this->httpFilePointer === false) {
113 | $error = error_get_last();
114 | throw HTTPException::connectionFailure(
115 | $this->options['ip'],
116 | $this->options['port'],
117 | $error['message'],
118 | 0
119 | );
120 | }
121 | }
122 |
123 | /**
124 | * @param $connection
125 | *
126 | * @return array
127 | */
128 | public function getStreamHeaders($connection = null)
129 | {
130 | if ($connection == null) {
131 | $connection = $this->httpFilePointer;
132 | }
133 | $headers = [];
134 | if ($connection !== false) {
135 | $metaData = stream_get_meta_data($connection);
136 | // The structure of this array differs depending on PHP compiled with
137 | // --enable-curlwrappers or not. Both cases are normally required.
138 | $rawHeaders = isset($metaData['wrapper_data']['headers'])
139 | ? $metaData['wrapper_data']['headers'] : $metaData['wrapper_data'];
140 |
141 | foreach ($rawHeaders as $lineContent) {
142 | // Extract header values
143 | if (preg_match('(^HTTP/(?P\d+\.\d+)\s+(?P\d+))S', $lineContent, $match)) {
144 | $headers['version'] = $match['version'];
145 | $headers['status'] = (int) $match['status'];
146 | } else {
147 | list($key, $value) = explode(':', $lineContent, 2);
148 | $headers[strtolower($key)] = ltrim($value);
149 | }
150 | }
151 | }
152 |
153 | return $headers;
154 | }
155 |
156 | /**
157 | * Perform a request to the server and return the result.
158 | *
159 | * Perform a request to the server and return the result converted into a
160 | * Response object. If you do not expect a JSON structure, which
161 | * could be converted in such a response object, set the forth parameter to
162 | * true, and you get a response object returned, containing the raw body.
163 | *
164 | * @param string $method
165 | * @param string $path
166 | * @param string $data
167 | * @param bool $raw
168 | * @param array $headers
169 | *
170 | * @throws HTTPException
171 | *
172 | * @return Response
173 | */
174 | public function request($method, $path, $data = null, $raw = false, array $headers = [])
175 | {
176 | $fullPath = $path;
177 | if ($this->options['path']) {
178 | $fullPath = '/'.$this->options['path'].$path;
179 | }
180 |
181 | $this->checkConnection($method, $fullPath, $data, $headers);
182 |
183 | // Read request body.
184 | $body = '';
185 | while (!feof($this->httpFilePointer)) {
186 | $body .= fgets($this->httpFilePointer);
187 | }
188 |
189 | $headers = $this->getStreamHeaders();
190 |
191 | if (empty($headers['status'])) {
192 | throw HTTPException::readFailure(
193 | $this->options['ip'],
194 | $this->options['port'],
195 | 'Received an empty response or not status code',
196 | 0
197 | );
198 | }
199 |
200 | // Create response object from couch db response.
201 | if ($headers['status'] >= 400) {
202 | return new ErrorResponse($headers['status'], $headers, $body);
203 | }
204 |
205 | return new Response($headers['status'], $headers, $body, $raw);
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/Attachment.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | class Attachment
23 | {
24 | /**
25 | * Content-Type of the Attachment.
26 | *
27 | * If this is false on putting a new attachment into the database the
28 | * generic "application/octet-stream" type will be used.
29 | *
30 | * @var string
31 | */
32 | private $contentType = false;
33 |
34 | /**
35 | * Base64 Encoded tring of the Data.
36 | *
37 | * @var string
38 | */
39 | private $data;
40 |
41 | /**
42 | * @var string
43 | */
44 | private $binaryData;
45 |
46 | /**
47 | * This attachment only represented as stub, which means the attachment is standalone and not inline.
48 | *
49 | * WARNING: Never change this variable from true to false if you don't provide data.
50 | * CouchDB otherwise quits with an error: {"error":"unknown_error","reason":"function_clause"}
51 | *
52 | * @var bool
53 | */
54 | private $stub = true;
55 |
56 | /**
57 | * Size of the attachment.
58 | *
59 | * @var int
60 | */
61 | private $length = 0;
62 |
63 | /**
64 | * Revision Position field of this Attachment.
65 | *
66 | * @var int
67 | */
68 | private $revpos = 0;
69 |
70 | /**
71 | * @var Client
72 | */
73 | private $httpClient;
74 |
75 | /**
76 | * @var string
77 | */
78 | private $path;
79 |
80 | /**
81 | * Get the content-type of this attachment.
82 | *
83 | * @return string
84 | */
85 | public function getContentType()
86 | {
87 | return $this->contentType;
88 | }
89 |
90 | /**
91 | * Get the length of the base64 encoded representation of this attachment.
92 | *
93 | * @return int
94 | */
95 | public function getLength()
96 | {
97 | if (!$this->stub && !is_int($this->length)) {
98 | $this->length = strlen($this->data);
99 | }
100 |
101 | return $this->length;
102 | }
103 |
104 | /**
105 | * Get the raw data of this attachment.
106 | *
107 | * @return string
108 | */
109 | public function getRawData()
110 | {
111 | $this->lazyLoad();
112 |
113 | return $this->binaryData;
114 | }
115 |
116 | /**
117 | * @return string
118 | */
119 | public function getBase64EncodedData()
120 | {
121 | $this->lazyLoad();
122 |
123 | return $this->data;
124 | }
125 |
126 | /**
127 | * Lazy Load Data from CouchDB if necessary.
128 | *
129 | * @return void
130 | */
131 | private function lazyLoad()
132 | {
133 | if ($this->stub) {
134 | $response = $this->httpClient->request('GET', $this->path, null, true); // raw request
135 | if ($response->status != 200) {
136 | throw HTTPException::fromResponse($this->path, $response);
137 | }
138 | $this->stub = false;
139 | $this->binaryData = $response->body;
140 | $this->data = \base64_encode($this->binaryData);
141 | }
142 | }
143 |
144 | public function isLoaded()
145 | {
146 | return !$this->stub;
147 | }
148 |
149 | /**
150 | * Number of times an attachment was alreaady saved with the document, indicating in which revision it was added.
151 | *
152 | * @return int
153 | */
154 | public function getRevPos()
155 | {
156 | return $this->revpos;
157 | }
158 |
159 | /**
160 | * Attachments are special in how they need to be persisted depending on stub or not.
161 | *
162 | * TODO: Is this really necessary with all this special logic? Having attahments as special
163 | * case without special code would be really awesome.
164 | *
165 | * @return string
166 | */
167 | public function toArray()
168 | {
169 | if ($this->stub) {
170 | $json = ['stub' => true];
171 | } else {
172 | $json = ['data' => $this->getBase64EncodedData()];
173 | if ($this->contentType) {
174 | $json['content_type'] = $this->contentType;
175 | }
176 | }
177 |
178 | return $json;
179 | }
180 |
181 | /**
182 | * @param string $binaryData
183 | * @param string $base64Data
184 | * @param string $contentType
185 | * @param int $length
186 | * @param int $revPos
187 | * @param Client $httpClient
188 | * @param string $path
189 | */
190 | final private function __construct($binaryData = null, $base64Data = null, $contentType = false, $length = false, $revPos = false, $httpClient = null, $path = null)
191 | {
192 | if ($binaryData || $base64Data) {
193 | $this->binaryData = $binaryData;
194 | $this->data = $base64Data;
195 | $this->stub = false;
196 | } else {
197 | $this->stub = true;
198 | }
199 | $this->contentType = $contentType;
200 | $this->length = $length;
201 | $this->revpos = $revPos;
202 | $this->httpClient = $httpClient;
203 | $this->path = $path;
204 | }
205 |
206 | /**
207 | * Create an Attachment from a string or resource of binary data.
208 | *
209 | * WARNING: Changes to the file handle after calling this method will *NOT* be recognized anymore.
210 | *
211 | * @param string|resource $data
212 | * @param string $contentType
213 | *
214 | * @return Attachment
215 | */
216 | public static function createFromBinaryData($data, $contentType = false)
217 | {
218 | if (\is_resource($data)) {
219 | $data = \stream_get_contents($data);
220 | }
221 |
222 | return new self($data, \base64_encode($data), $contentType);
223 | }
224 |
225 | /**
226 | * Create an attachment from base64 data.
227 | *
228 | * @param string $data
229 | * @param string $contentType
230 | * @param int $revpos
231 | *
232 | * @return Attachment
233 | */
234 | public static function createFromBase64Data($data, $contentType = false, $revpos = false)
235 | {
236 | return new self(\base64_decode($data), $data, $contentType, false, $revpos);
237 | }
238 |
239 | /**
240 | * Create a stub attachment that has lazy loading capabilities.
241 | *
242 | * @param string $contentType
243 | * @param int $length
244 | * @param int $revPos
245 | * @param Client $httpClient
246 | * @param string $path
247 | *
248 | * @return Attachment
249 | */
250 | public static function createStub($contentType, $length, $revPos, Client $httpClient, $path)
251 | {
252 | return new self(null, null, $contentType, $length, $revPos, $httpClient, $path);
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/tests/Doctrine/Tests/CouchDB/Functional/MangoTest.php:
--------------------------------------------------------------------------------
1 | createCouchDBClient();
12 | $client->deleteDatabase($this->getTestDatabase());
13 | sleep(0.5);
14 | $client->createDatabase($this->getTestDatabase());
15 | }
16 |
17 | public function testFind()
18 | {
19 | $client = $this->createMangoClient();
20 |
21 | $string = file_get_contents(__DIR__.'/../../datasets/shows.json');
22 | $shows = json_decode($string, true);
23 |
24 | $updater = $client->createBulkUpdater();
25 | $updater->updateDocuments($shows);
26 | $response = $updater->execute();
27 |
28 | foreach ($response->body as $key=>$row) {
29 | $shows[$key] = ['_id'=>$row['id'], '_rev'=>$row['rev']] + $shows[$key];
30 | }
31 |
32 | //Everything
33 | $query = new MangoQuery();
34 | $response = $client->find($query->limit(999));
35 |
36 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
37 | $this->assertObjectHasAttribute('body', $response);
38 | $this->assertArrayHasKey('docs', $response->body);
39 |
40 | $this->assertEquals($shows, $response->body['docs']);
41 |
42 | //Query by a field with no index
43 | $response = $client->find(new MangoQuery(['name'=>['$eq'=>'Under the Dome']]));
44 |
45 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
46 | $this->assertObjectHasAttribute('body', $response);
47 | $this->assertArrayHasKey('docs', $response->body);
48 | //No index found warning
49 | $this->assertArrayHasKey('warning', $response->body);
50 | $this->assertEquals([$shows[0]], $response->body['docs']);
51 |
52 |
53 | //Nothing
54 | $response = $client->find(new MangoQuery(['_id'=>['$eq'=>null]]));
55 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
56 | $this->assertObjectHasAttribute('body', $response);
57 | $this->assertArrayHasKey('docs', $response->body);
58 |
59 | $this->assertEquals([], $response->body['docs']);
60 |
61 | //Selector Basics
62 | $response = $client->find(new MangoQuery(['name'=>'Person of Interest']));
63 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
64 | $this->assertObjectHasAttribute('body', $response);
65 | $this->assertArrayHasKey('docs', $response->body);
66 | $this->assertEquals([$shows[1]], $response->body['docs']);
67 |
68 | //Selector with 2 fields
69 | $response = $client->find(new MangoQuery(['name'=>'Person of Interest', 'language'=>'English']));
70 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
71 | $this->assertObjectHasAttribute('body', $response);
72 | $this->assertArrayHasKey('docs', $response->body);
73 | $this->assertEquals([$shows[1]], $response->body['docs']);
74 |
75 | //Condition Operators
76 | $response = $client->find(new MangoQuery(['runtime'=>['$gt'=>60]]));
77 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
78 | $this->assertObjectHasAttribute('body', $response);
79 | $this->assertArrayHasKey('docs', $response->body);
80 | $this->assertEquals(2, count($response->body['docs']));
81 |
82 | //Subfields
83 | $response = $client->find(new MangoQuery(['rating'=>['average'=>8]]));
84 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
85 | $this->assertObjectHasAttribute('body', $response);
86 | $this->assertArrayHasKey('docs', $response->body);
87 | $this->assertEquals(9, count($response->body['docs']));
88 |
89 | //Subfield with dot notation
90 | $response = $client->find(new MangoQuery(['rating.average'=>8]));
91 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
92 | $this->assertObjectHasAttribute('body', $response);
93 | $this->assertArrayHasKey('docs', $response->body);
94 | $this->assertEquals(9, count($response->body['docs']));
95 |
96 | //Explicit $and
97 | $response = $client->find(new MangoQuery(
98 | ['$and'=> [
99 | [
100 | 'name'=> [
101 | '$eq'=> 'Under the Dome',
102 | ],
103 | 'genres'=> [
104 | '$in'=> ['Drama'],
105 | ],
106 | ],
107 | ],
108 | ]));
109 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
110 | $this->assertObjectHasAttribute('body', $response);
111 | $this->assertArrayHasKey('docs', $response->body);
112 | $this->assertEquals(1, count($response->body['docs']));
113 | $this->assertEquals([$shows[0]], $response->body['docs']);
114 |
115 | //$or operator
116 | $response = $client->find(new MangoQuery(
117 | [
118 | 'runtime'=> 60,
119 | '$or' => [
120 | [
121 | 'name'=> 'Under the Dome',
122 | ],
123 | [
124 | 'name'=> 'Person of Interest',
125 | ],
126 | ],
127 | ]));
128 |
129 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
130 | $this->assertObjectHasAttribute('body', $response);
131 | $this->assertArrayHasKey('docs', $response->body);
132 | $this->assertEquals(2, count($response->body['docs']));
133 | $this->assertEquals([$shows[0], $shows[1]], $response->body['docs']);
134 |
135 | //repeated key
136 | $response = $client->find(new MangoQuery(
137 | ['$and'=> [
138 | ['rating.average'=>['$gte'=>9]],
139 | ['rating.average'=> ['$lte'=>10]],
140 | ],
141 | ]));
142 |
143 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
144 | $this->assertObjectHasAttribute('body', $response);
145 | $this->assertArrayHasKey('docs', $response->body);
146 | $this->assertEquals(17, count($response->body['docs']));
147 |
148 | //Limits and skips
149 |
150 | //Get first
151 | $response = $client->find((new MangoQuery())->limit(1));
152 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
153 | $this->assertObjectHasAttribute('body', $response);
154 | $this->assertArrayHasKey('docs', $response->body);
155 | $this->assertEquals(1, count($response->body['docs']));
156 | $this->assertEquals([$shows[0]], $response->body['docs']);
157 |
158 | //Get second
159 | $response = $client->find((new MangoQuery())->limit(1)->skip(1));
160 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
161 | $this->assertObjectHasAttribute('body', $response);
162 | $this->assertArrayHasKey('docs', $response->body);
163 | $this->assertEquals(1, count($response->body['docs']));
164 | $this->assertEquals([$shows[1]], $response->body['docs']);
165 |
166 | //Select fields
167 | $expected = [
168 | [
169 | 'id' => $shows[1]['id'],
170 | 'name'=> $shows[1]['name'],
171 | ],
172 | ];
173 |
174 | $query = new MangoQuery();
175 | $query->select(['id', 'name'])->skip(1)->limit(1);
176 |
177 | $response = $client->find($query);
178 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
179 | $this->assertObjectHasAttribute('body', $response);
180 | $this->assertArrayHasKey('docs', $response->body);
181 | $this->assertEquals(1, count($response->body['docs']));
182 | $this->assertEquals($expected, $response->body['docs']);
183 | }
184 |
185 | public function testMangoIndexAndSort()
186 | {
187 | //Fill database
188 | $client = $this->createMangoClient();
189 | $string = file_get_contents(__DIR__.'/../../datasets/shows.json');
190 | $shows = json_decode($string, true);
191 | $updater = $client->createBulkUpdater();
192 | $updater->updateDocuments($shows);
193 | $response = $updater->execute();
194 |
195 | //create index
196 | $fields = [['name'=>'desc']];
197 | $response = $client->createMangoIndex($fields, 'index-test', 'name-desc');
198 |
199 | $this->assertObjectHasAttribute('body', $response);
200 | $this->assertEquals('created', $response->body['result']);
201 | $this->assertEquals('_design/index-test', $response->body['id']);
202 | $this->assertEquals('name-desc', $response->body['name']);
203 |
204 | $response = $client->find(new MangoQuery(['name'=>['$eq'=>'Under the Dome']]));
205 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
206 | $this->assertObjectHasAttribute('body', $response);
207 | $this->assertArrayHasKey('docs', $response->body);
208 | $this->assertArrayNotHasKey('warning', $response->body);
209 |
210 | //Test sort
211 | $query = new MangoQuery(['name'=>['$gt'=>null]]);
212 | $query->sort([['name'=>'desc']]);
213 | $response = $client->find($query);
214 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
215 | $this->assertObjectHasAttribute('body', $response);
216 | $this->assertArrayHasKey('docs', $response->body);
217 | $this->assertArrayNotHasKey('warning', $response->body);
218 |
219 | $this->assertEquals('Z Nation', $response->body['docs'][0]['name']);
220 |
221 | $deleted = $client->deleteMangoIndex('index-test', 'name-desc');
222 | $this->assertTrue($deleted);
223 |
224 | //create subdocument index
225 | $fields = [['rating.average'=>'desc'], ['name'=>'desc']];
226 | $response = $client->createMangoIndex($fields, 'index-test', 'rating.average-desc');
227 | $query = new MangoQuery(['rating.average'=>['$gt'=>null]]);
228 | $query->sort($fields);
229 |
230 | $response = $client->find($query);
231 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
232 | $this->assertObjectHasAttribute('body', $response);
233 | $this->assertArrayHasKey('docs', $response->body);
234 | $this->assertArrayNotHasKey('warning', $response->body);
235 | $this->assertEquals('The Wire', $response->body['docs'][0]['name']);
236 |
237 | //Create another index in the same document
238 | $fields = [['type'=>'asc'], ['name'=>'asc']];
239 | $response = $client->createMangoIndex($fields, 'index-test', 'type-asc&name-asc');
240 | $this->assertObjectHasAttribute('body', $response);
241 | $this->assertEquals('created', $response->body['result']);
242 | $this->assertEquals('_design/index-test', $response->body['id']);
243 | $this->assertEquals('type-asc&name-asc', $response->body['name']);
244 | $query = new MangoQuery(['type'=>['$gt'=>null]]);
245 | $query->sort($fields);
246 | $response = $client->find($query);
247 | $this->assertEquals('American Dad!', $response->body['docs'][0]['name']);
248 |
249 | //Find for impacts
250 | $query = new MangoQuery(['rating.average'=>['$gt'=>null]]);
251 | $query->sort([['rating.average'=>'desc'], ['name'=>'desc']]);
252 | $response = $client->find($query);
253 | $this->assertObjectHasAttribute('body', $response);
254 | $this->assertArrayHasKey('docs', $response->body);
255 | $this->assertArrayNotHasKey('warning', $response->body);
256 | $this->assertEquals('The Wire', $response->body['docs'][0]['name']);
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/HTTP/SocketClient.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | class SocketClient extends AbstractHTTPClient
18 | {
19 | /**
20 | * Connection pointer for connections, once keep alive is working on the
21 | * CouchDb side.
22 | *
23 | * @var resource
24 | */
25 | protected $connection;
26 |
27 | /**
28 | * Return the socket after setting up the connection to server and writing
29 | * the headers. The returned resource can later be used to read and
30 | * write data in chunks. These should be done without much delay as the
31 | * connection might get closed.
32 | *
33 | * @throws HTTPException
34 | *
35 | * @return resource
36 | */
37 | public function getConnection(
38 | $method,
39 | $path,
40 | $data = null,
41 | array $headers = []
42 | ) {
43 | $fullPath = $path;
44 | if ($this->options['path']) {
45 | $fullPath = '/'.$this->options['path'].$path;
46 | }
47 |
48 | $this->checkConnection();
49 | $stringHeader = $this->buildRequest($method, $fullPath, $data, $headers);
50 | // Send the build request to the server
51 | if (fwrite($this->connection, $stringHeader) === false) {
52 | // Reestablish which seems to have been aborted
53 | //
54 | // The recursion in this method might be problematic if the
55 | // connection establishing mechanism does not correctly throw an
56 | // exception on failure.
57 | $this->connection = null;
58 |
59 | return $this->getConnection($method, $path, $data, $headers);
60 | }
61 |
62 | return $this->connection;
63 | }
64 |
65 | /**
66 | * Check for server connection.
67 | *
68 | * Checks if the connection already has been established, or tries to
69 | * establish the connection, if not done yet.
70 | *
71 | * @throws HTTPException
72 | *
73 | * @return void
74 | */
75 | protected function checkConnection()
76 | {
77 | // Setting Connection scheme according ssl support
78 | if ($this->options['ssl']) {
79 | if (!extension_loaded('openssl')) {
80 | // no openssl extension loaded.
81 | // This is a bit hackisch...
82 | $this->connection = null;
83 |
84 | throw HTTPException::connectionFailure(
85 | $this->options['ip'],
86 | $this->options['port'],
87 | 'ssl activated without openssl extension loaded',
88 | 0
89 | );
90 | }
91 |
92 | $host = 'ssl://'.$this->options['host'];
93 | } else {
94 | $host = $this->options['ip'];
95 | }
96 |
97 | // If the connection could not be established, fsockopen sadly does not
98 | // only return false (as documented), but also always issues a warning.
99 | if (($this->connection === null) &&
100 | (($this->connection = @fsockopen($host, $this->options['port'], $errno, $errstr, $this->options['timeout'])) === false)) {
101 | // This is a bit hackisch...
102 | $this->connection = null;
103 | throw HTTPException::connectionFailure(
104 | $this->options['ip'],
105 | $this->options['port'],
106 | $errstr,
107 | $errno
108 | );
109 | }
110 | }
111 |
112 | /**
113 | * Build a HTTP 1.1 request.
114 | *
115 | * Build the HTTP 1.1 request headers from the given input.
116 | *
117 | * @param string $method
118 | * @param string $path
119 | * @param string $data
120 | * @param array $headers
121 | *
122 | * @return string
123 | */
124 | protected function buildRequest(
125 | $method,
126 | $path,
127 | $data = null,
128 | array $headers = []
129 | ) {
130 | // Create basic request headers
131 | $host = "Host: {$this->options['host']}";
132 | if ($this->options['port'] != 80) {
133 | $host .= ":{$this->options['port']}";
134 | }
135 | $request = "$method $path HTTP/1.1\r\n$host\r\n";
136 |
137 | // Add basic auth if set
138 | if ($this->options['username']) {
139 | $request .= sprintf("Authorization: Basic %s\r\n",
140 | base64_encode($this->options['username'].':'.$this->options['password'])
141 | );
142 | }
143 |
144 | // Set keep-alive header, which helps to keep to connection
145 | // initialization costs low, especially when the database server is not
146 | // available in the locale net.
147 | $request .= 'Connection: '.($this->options['keep-alive'] ? 'Keep-Alive' : 'Close')."\r\n";
148 |
149 | if ($this->options['headers']) {
150 | $headers = array_merge($this->options['headers'], $headers);
151 | }
152 | if (!isset($headers['Content-Type'])) {
153 | $headers['Content-Type'] = 'application/json';
154 | }
155 | foreach ($headers as $key => $value) {
156 | if (is_bool($value) === true) {
157 | $value = ($value) ? 'true' : 'false';
158 | }
159 | $request .= $key.': '.$value."\r\n";
160 | }
161 |
162 | // Also add headers and request body if data should be sent to the
163 | // server. Otherwise just add the closing mark for the header section
164 | // of the request.
165 | if ($data !== null) {
166 | $request .= 'Content-Length: '.strlen($data)."\r\n\r\n";
167 | $request .= $data;
168 | } else {
169 | $request .= "\r\n";
170 | }
171 |
172 | return $request;
173 | }
174 |
175 | /**
176 | * Perform a request to the server and return the result.
177 | *
178 | * Perform a request to the server and return the result converted into a
179 | * Response object. If you do not expect a JSON structure, which
180 | * could be converted in such a response object, set the forth parameter to
181 | * true, and you get a response object returned, containing the raw body.
182 | *
183 | * @param string $method
184 | * @param string $path
185 | * @param string $data
186 | * @param bool $raw
187 | * @param array $headers
188 | *
189 | * @return Response
190 | */
191 | public function request($method, $path, $data = null, $raw = false, array $headers = [])
192 | {
193 | $fullPath = $path;
194 | if ($this->options['path']) {
195 | $fullPath = '/'.$this->options['path'].$path;
196 | }
197 |
198 | // Try establishing the connection to the server
199 | $this->checkConnection();
200 |
201 | // Send the build request to the server
202 | if (fwrite($this->connection, $request = $this->buildRequest($method, $fullPath, $data, $headers)) === false) {
203 | // Reestablish which seems to have been aborted
204 | //
205 | // The recursion in this method might be problematic if the
206 | // connection establishing mechanism does not correctly throw an
207 | // exception on failure.
208 | $this->connection = null;
209 |
210 | return $this->request($method, $path, $data, $raw, $headers);
211 | }
212 |
213 | // Read server response headers
214 | $rawHeaders = '';
215 | $headers = [
216 | 'connection' => ($this->options['keep-alive'] ? 'Keep-Alive' : 'Close'),
217 | ];
218 |
219 | // Remove leading newlines, should not occur at all, actually.
220 | while ((($line = fgets($this->connection)) !== false) &&
221 | (($lineContent = rtrim($line)) === ''));
222 |
223 | // Throw exception, if connection has been aborted by the server, and
224 | // leave handling to the user for now.
225 | if ($line === false) {
226 | // Reestablish which seems to have been aborted
227 | //
228 | // The recursion in this method might be problematic if the
229 | // connection establishing mechanism does not correctly throw an
230 | // exception on failure.
231 | //
232 | // An aborted connection seems to happen here on long running
233 | // requests, which cause a connection timeout at server side.
234 | $this->connection = null;
235 |
236 | return $this->request($method, $path, $data, $raw, $headers);
237 | }
238 |
239 | do {
240 | // Also store raw headers for later logging
241 | $rawHeaders .= $lineContent."\n";
242 |
243 | // Extract header values
244 | if (preg_match('(^HTTP/(?P\d+\.\d+)\s+(?P\d+))S', $lineContent, $match)) {
245 | $headers['version'] = $match['version'];
246 | $headers['status'] = (int) $match['status'];
247 | } else {
248 | list($key, $value) = explode(':', $lineContent, 2);
249 | $headers[strtolower($key)] = ltrim($value);
250 | }
251 | } while ((($line = fgets($this->connection)) !== false) &&
252 | (($lineContent = rtrim($line)) !== ''));
253 |
254 | $body = '';
255 | // Read response body for NON-HEAD requests
256 | if(strtoupper($method) !== 'HEAD'){
257 | if (!isset($headers['transfer-encoding']) ||
258 | ($headers['transfer-encoding'] !== 'chunked')) {
259 | // HTTP 1.1 supports chunked transfer encoding, if the according
260 | // header is not set, just read the specified amount of bytes.
261 | $bytesToRead = (int) (isset($headers['content-length']) ? $headers['content-length'] : 0);
262 |
263 | // Read body only as specified by chunk sizes, everything else
264 | // are just footnotes, which are not relevant for us.
265 | while ($bytesToRead > 0) {
266 | $body .= $read = fgets($this->connection, $bytesToRead + 1);
267 | $bytesToRead -= strlen($read);
268 | }
269 | } else {
270 | // When transfer-encoding=chunked has been specified in the
271 | // response headers, read all chunks and sum them up to the body,
272 | // until the server has finished. Ignore all additional HTTP
273 | // options after that.
274 | do {
275 | $line = rtrim(fgets($this->connection));
276 |
277 | // Get bytes to read, with option appending comment
278 | if (preg_match('(^([0-9a-f]+)(?:;.*)?$)', $line, $match)) {
279 | $bytesToRead = hexdec($match[1]);
280 |
281 | // Read body only as specified by chunk sizes, everything else
282 | // are just footnotes, which are not relevant for us.
283 | $bytesLeft = $bytesToRead;
284 | while ($bytesLeft > 0) {
285 | $body .= $read = fread($this->connection, $bytesLeft + 2);
286 | $bytesLeft -= strlen($read);
287 | }
288 | }
289 | } while ($bytesToRead > 0);
290 |
291 | // Chop off \r\n from the end.
292 | $body = substr($body, 0, -2);
293 | }
294 | }
295 |
296 | // Reset the connection if the server asks for it.
297 | if ($headers['connection'] !== 'Keep-Alive') {
298 | fclose($this->connection);
299 | $this->connection = null;
300 | }
301 |
302 | // Handle some response state as special cases
303 | switch ($headers['status']) {
304 | case 301:
305 | case 302:
306 | case 303:
307 | case 307:
308 | $path = parse_url($headers['location'], PHP_URL_PATH);
309 |
310 | return $this->request($method, $path, $data, $raw, $headers);
311 | }
312 |
313 | // Create response object from couch db response
314 | if ($headers['status'] >= 400) {
315 | return new ErrorResponse($headers['status'], $headers, $body);
316 | }
317 |
318 | return new Response($headers['status'], $headers, $body, $raw);
319 | }
320 | }
321 |
--------------------------------------------------------------------------------
/tests/Doctrine/Tests/CouchDB/Functional/HTTP/MultipartParserAndSenderTest.php:
--------------------------------------------------------------------------------
1 | streamClientMock = $this->getMockBuilder(
26 | 'Doctrine\CouchDB\HTTP\StreamClient'
27 | )->disableOriginalConstructor()->getMock();
28 |
29 | $this->parserAndSender = new MultipartParserAndSender(
30 | $this->getHttpClient(),
31 | $this->getHttpClient()
32 | );
33 | // Set the protected $sourceClient of parserAndSender to the
34 | // streamClientMock.
35 | $reflector = new \ReflectionProperty(
36 | 'Doctrine\CouchDB\HTTP\MultipartParserAndSender',
37 | 'sourceClient'
38 | );
39 | $reflector->setAccessible(true);
40 | $reflector->setValue($this->parserAndSender, $this->streamClientMock);
41 |
42 | // Params for the request.
43 | $this->sourceParams = ['revs' => true, 'latest' => true];
44 | $this->sourceMethod = 'GET';
45 | $this->docId = 'multipartTestDoc';
46 | $this->sourcePath = '/'.$this->getTestDatabase().'/'.$this->docId;
47 | $this->targetPath = '/'.$this->getTestDatabase().'_multipart_copy'
48 | .'/'.$this->docId.'?new_edits=false';
49 | $this->sourceHeaders = ['Accept' => 'multipart/mixed'];
50 | }
51 |
52 | public function tearDown()
53 | {
54 | parent::tearDown();
55 | $this->createCouchDBClient()->deleteDatabase($this->getTestDatabase().'_multipart_copy');
56 | }
57 |
58 | public function testRequestThrowsHTTPExceptionOnEmptyStatus()
59 | {
60 | $this->setExpectedException(
61 | '\Doctrine\CouchDB\HTTP\HTTPException',
62 | sprintf(
63 | "Could read from server at %s:%d: '%d: %s'",
64 | '127.0.0.1',
65 | '5984',
66 | 0,
67 | 'Received an empty response or not status code'
68 | )
69 |
70 | );
71 | // Return header without status code.
72 | $this->streamClientMock->expects($this->once())
73 | ->method('getStreamHeaders')
74 | ->willReturn([]);
75 |
76 | $this->streamClientMock->expects($this->exactly(2))
77 | ->method('getOptions')
78 | ->will($this->onConsecutiveCalls(
79 | ['ip' => '127.0.0.1'],
80 | ['port' => '5984']
81 | ));
82 |
83 | $this->parserAndSender->request(
84 | $this->sourceMethod,
85 | $this->sourcePath,
86 | $this->targetPath,
87 | null,
88 | $this->sourceHeaders
89 | );
90 | }
91 |
92 | public function testRequestReturnsErrorResponseOnWrongStatusCode()
93 | {
94 | // Return header without status code > 400.
95 | $this->streamClientMock->expects($this->once())
96 | ->method('getStreamHeaders')
97 | ->willReturn(['status' => 404]);
98 |
99 | $string = 'This is the sample body of the response from the source.\n
100 | It has two lines.';
101 | $stream = fopen('data://text/plain,'.$string, 'r');
102 | $this->streamClientMock->expects($this->once())
103 | ->method('getConnection')
104 | ->willReturn($stream);
105 |
106 | $response = $this->parserAndSender->request(
107 | $this->sourceMethod,
108 | $this->sourcePath,
109 | $this->targetPath,
110 | null,
111 | $this->sourceHeaders
112 | );
113 |
114 | $this->AssertEquals(
115 | new ErrorResponse(
116 | '404',
117 | ['status' => 404],
118 | $string
119 | ),
120 | $response
121 | );
122 | }
123 |
124 | /**
125 | * @expectedException \UnexpectedValueException
126 | * @expectedExceptionMessage This value is not supported.
127 | */
128 | public function testRequestThrowsExceptionOnUnsupportedContentType()
129 | {
130 | // Return header with status code as 200.
131 | $this->streamClientMock->expects($this->once())
132 | ->method('getStreamHeaders')
133 | ->willReturn(['status' => 200]);
134 | $string = <<<'EOT'
135 | --7b1596fc4940bc1be725ad67f11ec1c4
136 | Content-Type: HTML
137 | EOT;
138 | $stream = fopen('data://text/plain,'.$string, 'r');
139 | $this->streamClientMock->expects($this->once())
140 | ->method('getConnection')
141 | ->willReturn($stream);
142 |
143 | $response = $this->parserAndSender->request(
144 | $this->sourceMethod,
145 | $this->sourcePath,
146 | $this->targetPath,
147 | null,
148 | $this->sourceHeaders
149 | );
150 | }
151 |
152 | /**
153 | * @expectedException \Exception
154 | * @expectedExceptionMessage Unknown parameter with Content-Type.
155 | */
156 | public function testRequestThrowsExceptionOnUnknownParamWithContentType()
157 | {
158 | // Return header with status code as 200.
159 | $this->streamClientMock->expects($this->once())
160 | ->method('getStreamHeaders')
161 | ->willReturn(['status' => 200]);
162 | $string = <<<'EOT'
163 | --7b1596fc4940bc1be725ad67f11ec1c4
164 | Content-Type: application/json; unknownBlahBlah="true"
165 | EOT;
166 | $stream = fopen('data://text/plain,'.$string, 'r');
167 | $this->streamClientMock->expects($this->once())
168 | ->method('getConnection')
169 | ->willReturn($stream);
170 |
171 | $response = $this->parserAndSender->request(
172 | $this->sourceMethod,
173 | $this->sourcePath,
174 | $this->targetPath,
175 | null,
176 | $this->sourceHeaders
177 | );
178 | }
179 |
180 | public function testRequestSuccessWithoutAttachment()
181 | {
182 | // Return header with status code as 200.
183 | $this->streamClientMock->expects($this->once())
184 | ->method('getStreamHeaders')
185 | ->willReturn(['status' => 200]);
186 | $docs = [
187 | '{"_id": "'.$this->docId.'","_rev": "1-abc","foo":"bar"}',
188 | '{"_id": "'.$this->docId.'","_rev": "1-abcd","foo":"baz"}',
189 | '{"_id": "'.$this->docId.'","_rev": "1-abcde","foo":"baz"}',
190 | ];
191 | $string = <<streamClientMock->expects($this->once())
212 | ->method('getConnection')
213 | ->willReturn($stream);
214 |
215 | $response = $this->parserAndSender->request(
216 | $this->sourceMethod,
217 | $this->sourcePath,
218 | $this->targetPath,
219 | null,
220 | $this->sourceHeaders
221 | );
222 | // The returned response should have the JSON docs. The missing
223 | // revision at the source will be skipped.
224 | $this->AssertEquals(2, count($response));
225 | $this->AssertEquals(3, count($response[0]));
226 | $this->AssertEquals($docs[0], $response[0][0]);
227 | $this->AssertEquals($docs[1], $response[0][1]);
228 | $this->AssertEquals($docs[2], $response[0][2]);
229 | }
230 |
231 | public function testRequestSuccessWithAttachments()
232 | {
233 | $client = new CouchDBClient(
234 | $this->getHttpClient(),
235 | $this->getTestDatabase()
236 | );
237 | // Recreate DB
238 | $client->deleteDatabase($this->getTestDatabase());
239 | $client->createDatabase($this->getTestDatabase());
240 |
241 | // Doc id.
242 | $id = $this->docId;
243 | // Document with attachments.
244 | $docWithAttachment = [
245 | '_id' => $id,
246 | '_rev' => '1-abc',
247 | '_attachments' => [
248 | 'foo.txt' => [
249 | 'content_type' => 'text/plain',
250 | 'data' => 'VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=',
251 | ],
252 | 'bar.txt' => [
253 | 'content_type' => 'text/plain',
254 | 'data' => 'VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=',
255 | ],
256 | ],
257 | ];
258 | // Doc without any attachment. The id of both the docs is same.
259 | // So we will get two leaf revisions.
260 | $doc = ['_id' => $id, 'foo' => 'bar', '_rev' => '1-bcd'];
261 |
262 | // Add the documents to the test db using Bulk API.
263 | $updater = $client->createBulkUpdater();
264 | $updater->updateDocument($docWithAttachment);
265 | $updater->updateDocument($doc);
266 | // Set newedits to false to use the supplied _rev instead of assigning
267 | // new ones.
268 | $updater->setNewEdits(false);
269 | $response = $updater->execute();
270 |
271 | // Create the copy database and a copyClient to interact with it.
272 | $copyDb = $this->getTestDatabase().'_multipart_copy';
273 | $client->createDatabase($copyDb);
274 | $copyClient = new CouchDBClient($client->getHttpClient(), $copyDb);
275 |
276 | // Missing revisions in the $copyDb.
277 | $missingRevs = ['1-abc', '1-bcd'];
278 | $this->sourceParams['open_revs'] = json_encode($missingRevs);
279 | $query = http_build_query($this->sourceParams);
280 | $this->sourcePath .= '?'.$query;
281 |
282 | // Get the multipart data stream from real CouchDB instance.
283 | $stream = (new StreamClient())->getConnection(
284 | $this->sourceMethod,
285 | $this->sourcePath,
286 | null,
287 | $this->sourceHeaders
288 | );
289 |
290 | // Set the return values for the mocked StreamClient.
291 | $this->streamClientMock->expects($this->once())
292 | ->method('getConnection')
293 | ->willReturn($stream);
294 | // Return header with status code as 200.
295 | $this->streamClientMock->expects($this->once())
296 | ->method('getStreamHeaders')
297 | ->willReturn(['status' => 200]);
298 |
299 | // Transfer the missing revisions from the source to the target.
300 | list($docStack, $responses) = $this->parserAndSender->request(
301 | $this->sourceMethod,
302 | $this->sourcePath,
303 | $this->targetPath,
304 | null,
305 | $this->sourceHeaders
306 |
307 | );
308 | // $docStack should contain the doc that didn't have the attachment.
309 | $this->assertEquals(1, count($docStack));
310 | $this->assertEquals($doc, json_decode($docStack[0], true));
311 |
312 | // The doc with attachment should have been copied to the copyDb.
313 | $this->assertEquals(1, count($responses));
314 | $this->assertArrayHasKey('ok', $responses[0]);
315 | $this->assertEquals(true, $responses[0]['ok']);
316 | // Clean up.
317 | $client->deleteDatabase($this->getTestDatabase());
318 | $client->createDatabase($this->getTestDatabase());
319 | $client->deleteDatabase($copyDb);
320 | }
321 |
322 | /**
323 | * Test multipart request with body size in the request body.
324 | */
325 | public function testMultipartRequestWithSize()
326 | {
327 | $this->streamClientMock->expects($this->once())
328 | ->method('getStreamHeaders')
329 | ->willReturn(['status' => 200]);
330 | $docs = [
331 | '{"_id": "'.$this->docId.'","_rev": "1-abc","foo":"bar"}',
332 | '{"_id": "'.$this->docId.'","_rev": "1-abcd","foo":"baz"}',
333 | '{"_id": "'.$this->docId.'","_rev": "1-abcde","foo":"baz"}',
334 | ];
335 | $string = <<streamClientMock->expects($this->once())
359 | ->method('getConnection')
360 | ->willReturn($stream);
361 |
362 | $response = $this->parserAndSender->request(
363 | $this->sourceMethod,
364 | $this->sourcePath,
365 | $this->targetPath,
366 | null,
367 | $this->sourceHeaders
368 | );
369 | // The returned response should have the JSON docs.
370 | $this->AssertEquals(2, count($response));
371 | $this->AssertEquals(3, count($response[0]));
372 | $this->AssertEquals($docs[0], $response[0][0]);
373 | $this->AssertEquals($docs[1], $response[0][1]);
374 | $this->AssertEquals($docs[2], $response[0][2]);
375 | }
376 | }
377 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/HTTP/MultipartParserAndSender.php:
--------------------------------------------------------------------------------
1 | getOptions();
42 | $this->sourceClient = new StreamClient(
43 | $sourceOptions['host'],
44 | $sourceOptions['port'],
45 | $sourceOptions['username'],
46 | $sourceOptions['password'],
47 | $sourceOptions['ip'],
48 | $sourceOptions['ssl'],
49 | $sourceOptions['path'],
50 | $sourceOptions['timeout'],
51 | $sourceOptions['headers']
52 | );
53 |
54 | $targetOptions = $target->getOptions();
55 | $this->targetClient = new SocketClient(
56 | $targetOptions['host'],
57 | $targetOptions['port'],
58 | $targetOptions['username'],
59 | $targetOptions['password'],
60 | $targetOptions['ip'],
61 | $targetOptions['ssl'],
62 | $targetOptions['path'],
63 | $targetOptions['timeout'],
64 | $sourceOptions['headers']
65 | );
66 | }
67 |
68 | /**
69 | * Perform request to the source, parse the multipart response,
70 | * stream the documents with attachments to the target and return
71 | * the responses along with docs that did not have any attachments.
72 | *
73 | * @param string $sourceMethod
74 | * @param string $sourcePath
75 | * @param string $targetPath
76 | * @param string $sourceData
77 | * @param array $sourceHeaders
78 | *
79 | * @throws HTTPException
80 | * @throws \Exception
81 | *
82 | * @return array|ErrorResponse|string
83 | */
84 | public function request(
85 | $sourceMethod,
86 | $sourcePath,
87 | $targetPath,
88 | $sourceData = null,
89 | array $sourceHeaders = []
90 | ) {
91 | $this->sourceConnection = $this->sourceClient->getConnection(
92 | $sourceMethod,
93 | $sourcePath,
94 | $sourceData,
95 | $sourceHeaders
96 | );
97 | $sourceResponseHeaders = $this->sourceClient->getStreamHeaders();
98 | $body = '';
99 |
100 | if (empty($sourceResponseHeaders['status'])) {
101 | try {
102 | // Close the connection resource.
103 | fclose($this->sourceConnection);
104 | } catch (\Exception $e) {
105 | }
106 | throw HTTPException::readFailure(
107 | $this->sourceClient->getOptions()['ip'],
108 | $this->sourceClient->getOptions()['port'],
109 | 'Received an empty response or not status code',
110 | 0
111 | );
112 | } elseif ($sourceResponseHeaders['status'] != 200) {
113 | while (!feof($this->sourceConnection)) {
114 | $body .= fgets($this->sourceConnection);
115 | }
116 | try {
117 | fclose($this->sourceConnection);
118 | } catch (\Exception $e) {
119 | }
120 |
121 | return new ErrorResponse(
122 | $sourceResponseHeaders['status'],
123 | $sourceResponseHeaders,
124 | $body
125 | );
126 | } else {
127 | try {
128 | // Body is an array containing:
129 | // 1) Array of json string documents that don't have
130 | // attachments. These should be posted using the Bulk API.
131 | // 2) Responses of posting docs with attachments.
132 | $body = $this->parseAndSend($targetPath);
133 | try {
134 | fclose($this->sourceConnection);
135 | } catch (\Exception $e) {
136 | }
137 |
138 | return $body;
139 | } catch (\Exception $e) {
140 | throw $e;
141 | }
142 | }
143 | }
144 |
145 | /**
146 | * Read and return next line from the connection pointer.
147 | * $maxLength parameter can be used to set the maximum length
148 | * to be read.
149 | *
150 | * @param int $maxLength
151 | *
152 | * @return string
153 | */
154 | protected function getNextLineFromSourceConnection($maxLength = null)
155 | {
156 | if ($maxLength !== null) {
157 | return fgets($this->sourceConnection, $maxLength);
158 | } else {
159 | return fgets($this->sourceConnection);
160 | }
161 | }
162 |
163 | /**
164 | * Parses multipart data. Returns an array having:
165 | * 1) Array of json docs(which are strings) that don't have attachments.
166 | * These should be posted using the Bulk API.
167 | * 2) Responses of posting docs with attachments.
168 | *
169 | * @param $targetPath
170 | *
171 | * @throws \Exception
172 | * @throws \HTTPException
173 | *
174 | * @return array
175 | */
176 | protected function parseAndSend($targetPath)
177 | {
178 | // Main response boundary of the multipart stream.
179 | $mainBoundary = trim($this->getNextLineFromSourceConnection());
180 | // If on the first line we have the size, then main boundary should be
181 | // on the second line.
182 | if (is_numeric($mainBoundary)) {
183 | $mainBoundary = trim($this->getNextLineFromSourceConnection());
184 | }
185 |
186 | // Docs that don't have attachment.
187 | // These should be posted using Bulk upload.
188 | $docStack = [];
189 |
190 | // Responses from posting docs that have attachments.
191 | $responses = [];
192 |
193 | while (!feof($this->sourceConnection)) {
194 | $line = ltrim($this->getNextLineFromSourceConnection());
195 | if ($line == '') {
196 | continue;
197 | } elseif (strpos($line, 'Content-Type') !== false) {
198 | list($header, $value) = explode(':', $line);
199 | $header = trim($header);
200 | $value = trim($value);
201 | $boundary = '';
202 |
203 | if (strpos($value, ';') !== false) {
204 | list($type, $info) = explode(';', $value, 2);
205 | $info = trim($info);
206 |
207 | // Get the boundary for the current doc.
208 | if (strpos($info, 'boundary') !== false) {
209 | $boundary = $info;
210 | } elseif (strpos($info, 'error') !== false) {
211 |
212 | // Missing revs at the source. Continue till the end
213 | // of this document.
214 | while (strpos($this->getNextLineFromSourceConnection(), $mainBoundary) === false);
215 | continue;
216 | } else {
217 | throw new \Exception('Unknown parameter with Content-Type.');
218 | }
219 | }
220 | // Doc with attachments.
221 | if (strpos($value, 'multipart/related') !== false) {
222 | if ($boundary == '') {
223 | throw new \Exception('Boundary not set for multipart/related data.');
224 | }
225 |
226 | $boundary = explode('=', $boundary, 2)[1];
227 |
228 | try {
229 | $responses[] = $this->sendStream(
230 | 'PUT',
231 | $targetPath,
232 | $mainBoundary,
233 | ['Content-Type' => 'multipart/related; boundary='.$boundary]);
234 | } catch (\Exception $e) {
235 | $responses[] = $e;
236 | }
237 | } elseif ($value == 'application/json') {
238 | // JSON doc without attachment.
239 | $jsonDoc = '';
240 |
241 | while (trim(($jsonDoc = $this->getNextLineFromSourceConnection())) == '');
242 | array_push($docStack, trim($jsonDoc));
243 |
244 | // Continue till the end of this document.
245 | while (strpos($this->getNextLineFromSourceConnection(), $mainBoundary) === false);
246 | } else {
247 | throw new \UnexpectedValueException('This value is not supported.');
248 | }
249 | } else {
250 | throw new \Exception('The first line is not the Content-Type.');
251 | }
252 | }
253 |
254 | return [$docStack, $responses];
255 | }
256 |
257 | /**
258 | * Reads multipart data from sourceConnection and streams it to the
259 | * targetConnection.Returns the body of the request or the status code in
260 | * case there is no body.
261 | *
262 | * @param $method
263 | * @param $path
264 | * @param $streamEnd
265 | * @param array $requestHeaders
266 | *
267 | * @throws \Exception
268 | * @throws \HTTPException
269 | *
270 | * @return mixed|string
271 | */
272 | protected function sendStream(
273 | $method,
274 | $path,
275 | $streamEnd,
276 | $requestHeaders = []
277 | ) {
278 | $dataStream = $this->sourceConnection;
279 |
280 | // Read the json doc. Use _attachments field to find the total
281 | // Content-Length and create the request header with initial doc data.
282 | // At present CouchDB can't handle chunked data and needs
283 | // Content-Length header.
284 | $str = '';
285 | $jsonFlag = 0;
286 | $attachmentCount = 0;
287 | $totalAttachmentLength = 0;
288 | $streamLine = $this->getNextLineFromSourceConnection();
289 | while (
290 | $jsonFlag == 0 ||
291 | ($jsonFlag == 1 &&
292 | trim($streamLine) == ''
293 | )
294 | ) {
295 | $str .= $streamLine;
296 | if (strpos($streamLine, 'Content-Type: application/json') !== false) {
297 | $jsonFlag = 1;
298 | }
299 | $streamLine = $this->getNextLineFromSourceConnection();
300 | }
301 | $docBoundaryLength = strlen(explode('=', $requestHeaders['Content-Type'], 2)[1]) + 2;
302 | $json = json_decode($streamLine, true);
303 | foreach ($json['_attachments'] as $docName => $metaData) {
304 | // Quotes and a "/r/n"
305 | $totalAttachmentLength += strlen('Content-Disposition: attachment; filename=') + strlen($docName) + 4;
306 | $totalAttachmentLength += strlen('Content-Type: ') + strlen($metaData['content_type']) + 2;
307 | $totalAttachmentLength += strlen('Content-Length: ');
308 | if (isset($metaData['encoding'])) {
309 | $totalAttachmentLength += $metaData['encoded_length'] + strlen($metaData['encoded_length']) + 2;
310 | $totalAttachmentLength += strlen('Content-Encoding: ') + strlen($metaData['encoding']) + 2;
311 | } else {
312 | $totalAttachmentLength += $metaData['length'] + strlen($metaData['length']) + 2;
313 | }
314 | $totalAttachmentLength += 2;
315 | $attachmentCount++;
316 | }
317 |
318 | // Add Content-Length to the headers.
319 | $requestHeaders['Content-Length'] = strlen($str) + strlen($streamLine)
320 | + $totalAttachmentLength + $attachmentCount * (2 + $docBoundaryLength) + $docBoundaryLength + 2;
321 |
322 | if ($this->targetConnection == null) {
323 | $this->targetConnection = $this->targetClient->getConnection(
324 | $method,
325 | $path,
326 | null,
327 | $requestHeaders
328 | );
329 | }
330 | // Write the initial body data.
331 | fwrite($this->targetConnection, $str);
332 |
333 | // Write the rest of the data including attachments line by line or in
334 | // chunks.
335 | while (!feof($dataStream) &&
336 | ($streamEnd === null ||
337 | strpos($streamLine, $streamEnd) ===
338 | false
339 | )
340 | ) {
341 | $totalSent = 0;
342 | $length = strlen($streamLine);
343 | while ($totalSent != $length) {
344 | $sent = fwrite($this->targetConnection, substr($streamLine, $totalSent));
345 | if ($sent === false) {
346 | throw new \HTTPException('Stream write error.');
347 | } else {
348 | $totalSent += $sent;
349 | }
350 | }
351 | // Use maxLength while reading the data as there may be no newlines
352 | // in the binary and compressed attachments, or the lines may be
353 | // very long.
354 | $streamLine = $this->getNextLineFromSourceConnection(100000);
355 | }
356 |
357 | // Read response headers
358 | $rawHeaders = '';
359 | $headers = [
360 | 'connection' => ($this->targetClient->getOptions()['keep-alive'] ? 'Keep-Alive' : 'Close'),
361 | ];
362 |
363 | // Remove leading newlines, should not occur at all, actually.
364 | while ((($line = fgets($this->targetConnection)) !== false) &&
365 | (($lineContent = rtrim($line)) === ''));
366 |
367 | // Throw exception, if connection has been aborted by the server, and
368 | // leave handling to the user for now.
369 | if ($line === false) {
370 | // sendStream can't be called in recursion as the source stream can be
371 | // read only once.
372 | $error = error_get_last();
373 | throw HTTPException::connectionFailure(
374 | $this->targetClient->getOptions()['ip'],
375 | $this->targetClient->getOptions()['port'],
376 | $error['message'],
377 | 0
378 | );
379 | }
380 |
381 | do {
382 | // Also store raw headers for later logging
383 | $rawHeaders .= $lineContent."\n";
384 | // Extract header values
385 | if (preg_match('(^HTTP/(?P\d+\.\d+)\s+(?P\d+))S', $lineContent, $match)) {
386 | $headers['version'] = $match['version'];
387 | $headers['status'] = (int) $match['status'];
388 | } else {
389 | list($key, $value) = explode(':', $lineContent, 2);
390 | $headers[strtolower($key)] = ltrim($value);
391 | }
392 | } while ((($line = fgets($this->targetConnection)) !== false) &&
393 | (($lineContent = rtrim($line)) !== ''));
394 |
395 | // Read response body
396 | $body = '';
397 |
398 | // HTTP 1.1 supports chunked transfer encoding, if the according
399 | // header is not set, just read the specified amount of bytes.
400 | $bytesToRead = (int) (isset($headers['content-length']) ? $headers['content-length'] : 0);
401 | // Read body only as specified by chunk sizes, everything else
402 | // are just footnotes, which are not relevant for us.
403 | while ($bytesToRead > 0) {
404 | $body .= $read = fgets($this->targetConnection, $bytesToRead + 1);
405 | $bytesToRead -= strlen($read);
406 | }
407 |
408 | // Reset the connection if the server asks for it.
409 | if ($headers['connection'] !== 'Keep-Alive') {
410 | fclose($this->targetConnection);
411 | $this->targetConnection = null;
412 | }
413 | // Handle some response state as special cases
414 | switch ($headers['status']) {
415 | case 301:
416 | case 302:
417 | case 303:
418 | case 307:
419 | // Temporary redirect.
420 | // sendStream can't be called in recursion as the source stream can be
421 | // read only once.
422 | throw HTTPException::fromResponse($path, new Response($headers['status'], $headers, $body));
423 | }
424 |
425 | return $body != '' ? json_decode($body, true) : ['status' => $headers['status']];
426 | }
427 | }
428 |
--------------------------------------------------------------------------------
/lib/Doctrine/CouchDB/CouchDBClient.php:
--------------------------------------------------------------------------------
1 |
22 | * @author Lukas Kahwe Smith
23 | */
24 | class CouchDBClient
25 | {
26 | /** string \ufff0 */
27 | const COLLATION_END = "\xEF\xBF\xB0";
28 |
29 | /**
30 | * Name of the CouchDB database.
31 | *
32 | * @string
33 | */
34 | protected $databaseName;
35 |
36 | /**
37 | * The underlying HTTP Connection of the used DocumentManager.
38 | *
39 | * @var Client
40 | */
41 | protected $httpClient;
42 |
43 | /**
44 | * CouchDB Version.
45 | *
46 | * @var string
47 | */
48 | protected $version = null;
49 |
50 | protected static $clients = [
51 | 'socket' => 'Doctrine\CouchDB\HTTP\SocketClient',
52 | 'stream' => 'Doctrine\CouchDB\HTTP\StreamClient',
53 | ];
54 |
55 | /**
56 | * Factory method for CouchDBClients.
57 | *
58 | * @param array $options
59 | *
60 | * @throws \InvalidArgumentException
61 | *
62 | * @return CouchDBClient
63 | */
64 | public static function create(array $options)
65 | {
66 | if (isset($options['url'])) {
67 | $urlParts = parse_url($options['url']);
68 |
69 | foreach ($urlParts as $part => $value) {
70 | switch ($part) {
71 | case 'host':
72 | case 'user':
73 | case 'port':
74 | $options[$part] = $value;
75 | break;
76 |
77 | case 'path':
78 | $path = explode('/', $value);
79 | $options['dbname'] = array_pop($path);
80 | $options['path'] = trim(implode('/', $path), '/');
81 | break;
82 |
83 | case 'pass':
84 | $options['password'] = $value;
85 | break;
86 |
87 | case 'scheme':
88 | $options['ssl'] = ($value === 'https');
89 | break;
90 |
91 | default:
92 | break;
93 | }
94 | }
95 | }
96 |
97 | if (!isset($options['dbname'])) {
98 | throw new \InvalidArgumentException("'dbname' is a required option to create a CouchDBClient");
99 | }
100 |
101 | $defaults = [
102 | 'type' => 'socket',
103 | 'host' => 'localhost',
104 | 'port' => 5984,
105 | 'user' => null,
106 | 'password' => null,
107 | 'ip' => null,
108 | 'ssl' => false,
109 | 'path' => null,
110 | 'logging' => false,
111 | 'timeout' => 10,
112 | 'headers' => [],
113 | ];
114 | $options = array_merge($defaults, $options);
115 |
116 | if (!isset(self::$clients[$options['type']])) {
117 | throw new \InvalidArgumentException(sprintf('There is no client implementation registered for %s, valid options are: %s',
118 | $options['type'], implode(', ', array_keys(self::$clients))
119 | ));
120 | }
121 | $connectionClass = self::$clients[$options['type']];
122 | $connection = new $connectionClass(
123 | $options['host'],
124 | $options['port'],
125 | $options['user'],
126 | $options['password'],
127 | $options['ip'],
128 | $options['ssl'],
129 | $options['path'],
130 | $options['timeout'],
131 | $options['headers']
132 | );
133 | if ($options['logging'] === true) {
134 | $connection = new HTTP\LoggingClient($connection);
135 | }
136 |
137 | return new static($connection, $options['dbname']);
138 | }
139 |
140 | /**
141 | * @param Client $client
142 | * @param string $databaseName
143 | */
144 | public function __construct(Client $client, $databaseName)
145 | {
146 | $this->httpClient = $client;
147 | $this->databaseName = $databaseName;
148 | }
149 |
150 | public function setHttpClient(Client $client)
151 | {
152 | $this->httpClient = $client;
153 | }
154 |
155 | /**
156 | * @return Client
157 | */
158 | public function getHttpClient()
159 | {
160 | return $this->httpClient;
161 | }
162 |
163 | public function getDatabase()
164 | {
165 | return $this->databaseName;
166 | }
167 |
168 | /**
169 | * Let CouchDB generate an array of UUIDs.
170 | *
171 | * @param int $count
172 | *
173 | * @throws CouchDBException
174 | *
175 | * @return array
176 | */
177 | public function getUuids($count = 1)
178 | {
179 | $count = (int) $count;
180 | $response = $this->httpClient->request('GET', '/_uuids?count='.$count);
181 |
182 | if ($response->status != 200) {
183 | throw new CouchDBException('Could not retrieve UUIDs from CouchDB.');
184 | }
185 |
186 | return $response->body['uuids'];
187 | }
188 |
189 |
190 | /**
191 | * Find a document by ID and return the HTTP response.
192 | *
193 | * @param string $id
194 | *
195 | * @return HTTP\Response
196 | */
197 | public function findDocument($id)
198 | {
199 | $documentPath = '/'.$this->databaseName.'/'.urlencode($id);
200 |
201 | return $this->httpClient->request('GET', $documentPath);
202 | }
203 |
204 | /**
205 | * Find documents of all or the specified revisions.
206 | *
207 | * If $revisions is an array containing the revisions to be fetched, only
208 | * the documents of those revisions are fetched. Else document of all
209 | * leaf revisions are fetched.
210 | *
211 | * @param string $docId
212 | * @param mixed $revisions
213 | *
214 | * @return HTTP\Response
215 | */
216 | public function findRevisions($docId, $revisions = null)
217 | {
218 | $path = '/'.$this->databaseName.'/'.urlencode($docId);
219 | if (is_array($revisions)) {
220 | // Fetch documents of only the specified leaf revisions.
221 | $path .= '?open_revs='.json_encode($revisions);
222 | } else {
223 | // Fetch documents of all leaf revisions.
224 | $path .= '?open_revs=all';
225 | }
226 | // Set the Accept header to application/json to get a JSON array in the
227 | // response's body. Without this the response is multipart/mixed stream.
228 | return $this->httpClient->request(
229 | 'GET',
230 | $path,
231 | null,
232 | false,
233 | ['Accept' => 'application/json']
234 | );
235 | }
236 |
237 | /**
238 | * Find many documents by passing their ids and return the HTTP response.
239 | *
240 | * @param array $ids
241 | * @param null $limit
242 | * @param null $offset
243 | *
244 | * @return HTTP\Response
245 | */
246 | public function findDocuments(array $ids, $limit = null, $offset = null)
247 | {
248 | $allDocsPath = '/'.$this->databaseName.'/_all_docs?include_docs=true';
249 | if ($limit) {
250 | $allDocsPath .= '&limit='.(int) $limit;
251 | }
252 | if ($offset) {
253 | $allDocsPath .= '&skip='.(int) $offset;
254 | }
255 |
256 | return $this->httpClient->request('POST', $allDocsPath, json_encode(
257 | ['keys' => array_values($ids)])
258 | );
259 | }
260 |
261 | /**
262 | * Get all documents.
263 | *
264 | * @param int|null $limit
265 | * @param string|null $startKey
266 | * @param string|null $endKey
267 | * @param int|null $skip
268 | * @param bool $descending
269 | *
270 | * @return HTTP\Response
271 | */
272 | public function allDocs($limit = null, $startKey = null, $endKey = null, $skip = null, $descending = false)
273 | {
274 | $allDocsPath = '/'.$this->databaseName.'/_all_docs?include_docs=true';
275 | if ($limit) {
276 | $allDocsPath .= '&limit='.(int) $limit;
277 | }
278 | if ($startKey) {
279 | $allDocsPath .= '&startkey="'.(string) $startKey.'"';
280 | }
281 | if (!is_null($endKey)) {
282 | $allDocsPath .= '&endkey="'.(string) $endKey.'"';
283 | }
284 | if (!is_null($skip) && (int) $skip > 0) {
285 | $allDocsPath .= '&skip='.(int) $skip;
286 | }
287 | if (!is_null($descending) && (bool) $descending === true) {
288 | $allDocsPath .= '&descending=true';
289 | }
290 |
291 | return $this->httpClient->request('GET', $allDocsPath);
292 | }
293 |
294 | /**
295 | * Get the current version of CouchDB.
296 | *
297 | * @throws HTTPException
298 | *
299 | * @return string
300 | */
301 | public function getVersion()
302 | {
303 | if ($this->version === null) {
304 | $response = $this->httpClient->request('GET', '/');
305 | if ($response->status != 200) {
306 | throw HTTPException::fromResponse('/', $response);
307 | }
308 |
309 | $this->version = $response->body['version'];
310 | }
311 |
312 | return $this->version;
313 | }
314 |
315 | /**
316 | * Get all databases.
317 | *
318 | * @throws HTTPException
319 | *
320 | * @return array
321 | */
322 | public function getAllDatabases()
323 | {
324 | $response = $this->httpClient->request('GET', '/_all_dbs');
325 | if ($response->status != 200) {
326 | throw HTTPException::fromResponse('/_all_dbs', $response);
327 | }
328 |
329 | return $response->body;
330 | }
331 |
332 | /**
333 | * Create a new database.
334 | *
335 | * @param string $name
336 | *
337 | * @throws HTTPException
338 | *
339 | * @return void
340 | */
341 | public function createDatabase($name)
342 | {
343 | $response = $this->httpClient->request('PUT', '/'.urlencode($name));
344 |
345 | if ($response->status != 201) {
346 | throw HTTPException::fromResponse('/'.urlencode($name), $response);
347 | }
348 | }
349 |
350 | /**
351 | * Drop a database.
352 | *
353 | * @param string $name
354 | *
355 | * @throws HTTPException
356 | *
357 | * @return void
358 | */
359 | public function deleteDatabase($name)
360 | {
361 | $response = $this->httpClient->request('DELETE', '/'.urlencode($name));
362 |
363 | if ($response->status != 200 && $response->status != 404) {
364 | throw HTTPException::fromResponse('/'.urlencode($name), $response);
365 | }
366 | }
367 |
368 | /**
369 | * Get Information about a database.
370 | *
371 | * @param string $name
372 | *
373 | * @throws HTTPException
374 | *
375 | * @return array
376 | */
377 | public function getDatabaseInfo($name = null)
378 | {
379 | $response = $this->httpClient->request('GET', '/'.($name ? urlencode($name) : $this->databaseName));
380 |
381 | if ($response->status != 200) {
382 | throw HTTPException::fromResponse('/'.urlencode($name), $response);
383 | }
384 |
385 | return $response->body;
386 | }
387 |
388 | /**
389 | * Get changes.
390 | *
391 | * @param array $params
392 | *
393 | * @throws HTTPException
394 | *
395 | * @return array
396 | */
397 | public function getChanges(array $params = [])
398 | {
399 | $path = '/'.$this->databaseName.'/_changes';
400 |
401 | $method = ((!isset($params['doc_ids']) || $params['doc_ids'] == null) ? 'GET' : 'POST');
402 | $response = '';
403 |
404 | if ($method == 'GET') {
405 | foreach ($params as $key => $value) {
406 | if (isset($params[$key]) === true && is_bool($value) === true) {
407 | $params[$key] = ($value) ? 'true' : 'false';
408 | }
409 | }
410 | if (count($params) > 0) {
411 | $query = http_build_query($params);
412 | $path = $path.'?'.$query;
413 | }
414 | $response = $this->httpClient->request('GET', $path);
415 | } else {
416 | $path .= '?filter=_doc_ids';
417 | $response = $this->httpClient->request('POST', $path, json_encode($params));
418 | }
419 | if ($response->status != 200) {
420 | throw HTTPException::fromResponse($path, $response);
421 | }
422 |
423 | return $response->body;
424 | }
425 |
426 | /**
427 | * Create a bulk updater instance.
428 | *
429 | * @return BulkUpdater
430 | */
431 | public function createBulkUpdater()
432 | {
433 | return new BulkUpdater($this->httpClient, $this->databaseName);
434 | }
435 |
436 | /**
437 | * Execute a POST request against CouchDB inserting a new document, leaving the server to generate a uuid.
438 | *
439 | * @param array $data
440 | *
441 | * @throws HTTPException
442 | *
443 | * @return array
444 | */
445 | public function postDocument(array $data)
446 | {
447 | $path = '/'.$this->databaseName;
448 | $response = $this->httpClient->request('POST', $path, json_encode($data));
449 |
450 | if ($response->status != 201) {
451 | throw HTTPException::fromResponse($path, $response);
452 | }
453 |
454 | return [$response->body['id'], $response->body['rev']];
455 | }
456 |
457 | /**
458 | * Execute a PUT request against CouchDB inserting or updating a document.
459 | *
460 | * @param array $data
461 | * @param string $id
462 | * @param string|null $rev
463 | *
464 | * @throws HTTPException
465 | *
466 | * @return array
467 | */
468 | public function putDocument($data, $id, $rev = null)
469 | {
470 | $data['_id'] = $id;
471 | if ($rev) {
472 | $data['_rev'] = $rev;
473 | }
474 |
475 | $path = '/'.$this->databaseName.'/'.urlencode($id);
476 | $response = $this->httpClient->request('PUT', $path, json_encode($data));
477 |
478 | if ($response->status != 201) {
479 | throw HTTPException::fromResponse($path, $response);
480 | }
481 |
482 | return [$response->body['id'], $response->body['rev']];
483 | }
484 |
485 | /**
486 | * Delete a document.
487 | *
488 | * @param string $id
489 | * @param string $rev
490 | *
491 | * @throws HTTPException
492 | *
493 | * @return void
494 | */
495 | public function deleteDocument($id, $rev)
496 | {
497 | $path = '/'.$this->databaseName.'/'.urlencode($id).'?rev='.$rev;
498 | $response = $this->httpClient->request('DELETE', $path);
499 |
500 | if ($response->status != 200) {
501 | throw HTTPException::fromResponse($path, $response);
502 | }
503 | }
504 |
505 | /**
506 | * @param string $designDocName
507 | * @param string $viewName
508 | * @param DesignDocument $designDoc
509 | *
510 | * @return View\Query
511 | */
512 | public function createViewQuery($designDocName, $viewName, DesignDocument $designDoc = null)
513 | {
514 | return new View\Query($this->httpClient, $this->databaseName, $designDocName, $viewName, $designDoc);
515 | }
516 |
517 | /**
518 | * Create or update a design document from the given in memory definition.
519 | *
520 | * @param string $designDocName
521 | * @param DesignDocument $designDoc
522 | *
523 | * @return HTTP\Response
524 | */
525 | public function createDesignDocument($designDocName, DesignDocument $designDoc)
526 | {
527 | $data = $designDoc->getData();
528 | $data['_id'] = '_design/'.$designDocName;
529 |
530 | $documentPath = '/'.$this->databaseName.'/'.$data['_id'];
531 | $response = $this->httpClient->request('GET', $documentPath);
532 |
533 | if ($response->status == 200) {
534 | $docData = $response->body;
535 | $data['_rev'] = $docData['_rev'];
536 | }
537 |
538 | return $this->httpClient->request(
539 | 'PUT',
540 | sprintf('/%s/_design/%s', $this->databaseName, $designDocName),
541 | json_encode($data)
542 | );
543 | }
544 |
545 | /**
546 | * GET /db/_compact.
547 | *
548 | * Return array of data about compaction status.
549 | *
550 | * @throws HTTPException
551 | *
552 | * @return array
553 | */
554 | public function getCompactInfo()
555 | {
556 | $path = sprintf('/%s/_compact', $this->databaseName);
557 | $response = $this->httpClient->request('GET', $path);
558 | if ($response->status >= 400) {
559 | throw HTTPException::fromResponse($path, $response);
560 | }
561 |
562 | return $response->body;
563 | }
564 |
565 | /**
566 | * POST /db/_compact.
567 | *
568 | * @throws HTTPException
569 | *
570 | * @return array
571 | */
572 | public function compactDatabase()
573 | {
574 | $path = sprintf('/%s/_compact', $this->databaseName);
575 | $response = $this->httpClient->request('POST', $path);
576 | if ($response->status >= 400) {
577 | throw HTTPException::fromResponse($path, $response);
578 | }
579 |
580 | return $response->body;
581 | }
582 |
583 | /**
584 | * POST /db/_compact/designDoc.
585 | *
586 | * @param string $designDoc
587 | *
588 | * @throws HTTPException
589 | *
590 | * @return array
591 | */
592 | public function compactView($designDoc)
593 | {
594 | $path = sprintf('/%s/_compact/%s', $this->databaseName, $designDoc);
595 | $response = $this->httpClient->request('POST', $path);
596 | if ($response->status >= 400) {
597 | throw HTTPException::fromResponse($path, $response);
598 | }
599 |
600 | return $response->body;
601 | }
602 |
603 | /**
604 | * POST /db/_view_cleanup.
605 | *
606 | * @throws HTTPException
607 | *
608 | * @return array
609 | */
610 | public function viewCleanup()
611 | {
612 | $path = sprintf('/%s/_view_cleanup', $this->databaseName);
613 | $response = $this->httpClient->request('POST', $path);
614 | if ($response->status >= 400) {
615 | throw HTTPException::fromResponse($path, $response);
616 | }
617 |
618 | return $response->body;
619 | }
620 |
621 | /**
622 | * POST /db/_replicate.
623 | *
624 | * @param string $source
625 | * @param string $target
626 | * @param bool|null $cancel
627 | * @param bool|null $continuous
628 | * @param string|null $filter
629 | * @param array|null $ids
630 | * @param string|null $proxy
631 | *
632 | * @throws HTTPException
633 | *
634 | * @return array
635 | */
636 | public function replicate($source, $target, $cancel = null, $continuous = null, $filter = null, array $ids = null, $proxy = null)
637 | {
638 | $params = ['target' => $target, 'source' => $source];
639 | if ($cancel !== null) {
640 | $params['cancel'] = (bool) $cancel;
641 | }
642 | if ($continuous !== null) {
643 | $params['continuous'] = (bool) $continuous;
644 | }
645 | if ($filter !== null) {
646 | $params['filter'] = $filter;
647 | }
648 | if ($ids !== null) {
649 | $params['doc_ids'] = $ids;
650 | }
651 | if ($proxy !== null) {
652 | $params['proxy'] = $proxy;
653 | }
654 | $path = '/_replicate';
655 | $response = $this->httpClient->request('POST', $path, json_encode($params));
656 | if ($response->status >= 400) {
657 | throw HTTPException::fromResponse($path, $response);
658 | }
659 |
660 | return $response->body;
661 | }
662 |
663 | /**
664 | * GET /_active_tasks.
665 | *
666 | * @throws HTTPException
667 | *
668 | * @return array
669 | */
670 | public function getActiveTasks()
671 | {
672 | $response = $this->httpClient->request('GET', '/_active_tasks');
673 | if ($response->status != 200) {
674 | throw HTTPException::fromResponse('/_active_tasks', $response);
675 | }
676 |
677 | return $response->body;
678 | }
679 |
680 | /**
681 | * Get revision difference.
682 | *
683 | * @param array $data
684 | *
685 | * @throws HTTPException
686 | *
687 | * @return array
688 | */
689 | public function getRevisionDifference($data)
690 | {
691 | $path = '/'.$this->databaseName.'/_revs_diff';
692 | $response = $this->httpClient->request('POST', $path, json_encode($data));
693 | if ($response->status != 200) {
694 | throw HTTPException::fromResponse($path, $response);
695 | }
696 |
697 | return $response->body;
698 | }
699 |
700 | /**
701 | * Transfer missing revisions to the target. The Content-Type of response
702 | * from the source should be multipart/mixed.
703 | *
704 | * @param string $docId
705 | * @param array $missingRevs
706 | * @param CouchDBClient $target
707 | *
708 | * @throws HTTPException
709 | *
710 | * @return array|HTTP\ErrorResponse|string
711 | */
712 | public function transferChangedDocuments($docId, $missingRevs, CouchDBClient $target)
713 | {
714 | $path = '/'.$this->getDatabase().'/'.$docId;
715 | $params = ['revs' => true, 'latest' => true, 'open_revs' => json_encode($missingRevs)];
716 | $query = http_build_query($params);
717 | $path .= '?'.$query;
718 |
719 | $targetPath = '/'.$target->getDatabase().'/'.$docId.'?new_edits=false';
720 |
721 | $mutltipartHandler = new MultipartParserAndSender($this->getHttpClient(), $target->getHttpClient());
722 |
723 | return $mutltipartHandler->request(
724 | 'GET',
725 | $path,
726 | $targetPath,
727 | null,
728 | ['Accept' => 'multipart/mixed']
729 | );
730 | }
731 |
732 | /**
733 | * Get changes as a stream.
734 | *
735 | * This method similar to the getChanges() method. But instead of returning
736 | * the set of changes, it returns the connection stream from which the response
737 | * can be read line by line. This is useful when you want to continuously get changes
738 | * as they occur. Filtered changes feed is not supported by this method.
739 | *
740 | * @param array $params
741 | * @param bool $raw
742 | *
743 | * @throws HTTPException
744 | *
745 | * @return resource
746 | */
747 | public function getChangesAsStream(array $params = [])
748 | {
749 | // Set feed to continuous.
750 | if (!isset($params['feed']) || $params['feed'] != 'continuous') {
751 | $params['feed'] = 'continuous';
752 | }
753 | $path = '/'.$this->databaseName.'/_changes';
754 | $connectionOptions = $this->getHttpClient()->getOptions();
755 | $streamClient = new StreamClient(
756 | $connectionOptions['host'],
757 | $connectionOptions['port'],
758 | $connectionOptions['username'],
759 | $connectionOptions['password'],
760 | $connectionOptions['ip'],
761 | $connectionOptions['ssl'],
762 | $connectionOptions['path']
763 | );
764 |
765 | foreach ($params as $key => $value) {
766 | if (isset($params[$key]) === true && is_bool($value) === true) {
767 | $params[$key] = ($value) ? 'true' : 'false';
768 | }
769 | }
770 | if (count($params) > 0) {
771 | $query = http_build_query($params);
772 | $path = $path.'?'.$query;
773 | }
774 | $stream = $streamClient->getConnection('GET', $path, null);
775 |
776 | $headers = $streamClient->getStreamHeaders($stream);
777 | if (empty($headers['status'])) {
778 | throw HTTPException::readFailure(
779 | $connectionOptions['ip'],
780 | $connectionOptions['port'],
781 | 'Received an empty response or not status code',
782 | 0
783 | );
784 | } elseif ($headers['status'] != 200) {
785 | $body = '';
786 | while (!feof($stream)) {
787 | $body .= fgets($stream);
788 | }
789 | throw HTTPException::fromResponse($path, new Response($headers['status'], $headers, $body));
790 | }
791 | // Everything seems okay. Return the connection resource.
792 | return $stream;
793 | }
794 |
795 | /**
796 | * Commit any recent changes to the specified database to disk.
797 | *
798 | * @throws HTTPException
799 | *
800 | * @return array
801 | */
802 | public function ensureFullCommit()
803 | {
804 | $path = '/'.$this->databaseName.'/_ensure_full_commit';
805 | $response = $this->httpClient->request('POST', $path);
806 | if ($response->status != 201) {
807 | throw HTTPException::fromResponse($path, $response);
808 | }
809 |
810 | return $response->body;
811 | }
812 | }
813 |
--------------------------------------------------------------------------------
/tests/Doctrine/Tests/CouchDB/Functional/CouchDBClientTest.php:
--------------------------------------------------------------------------------
1 | couchClient = $this->createCouchDBClient();
18 | $this->couchClient->deleteDatabase($this->getTestDatabase());
19 | sleep(0.5);
20 | $this->couchClient->createDatabase($this->getTestDatabase());
21 | }
22 |
23 | public function testGetUuids()
24 | {
25 | $uuids = $this->couchClient->getUuids();
26 | $this->assertEquals(1, count($uuids));
27 | $this->assertEquals(32, strlen($uuids[0]));
28 |
29 | $uuids = $this->couchClient->getUuids(10);
30 | $this->assertEquals(10, count($uuids));
31 | }
32 |
33 | public function testGetVersion()
34 | {
35 | $version = $this->couchClient->getVersion();
36 | $this->assertEquals(3, count(explode('.', $version)));
37 | }
38 |
39 | public function testGetAllDatabases()
40 | {
41 | $dbs = $this->couchClient->getAllDatabases();
42 | $this->assertContains($this->getTestDatabase(), $dbs);
43 | }
44 |
45 | public function testDeleteDatabase()
46 | {
47 | $this->couchClient->deleteDatabase($this->getTestDatabase());
48 |
49 | $dbs = $this->couchClient->getAllDatabases();
50 | $this->assertNotContains($this->getTestDatabase(), $dbs);
51 | }
52 |
53 | /**
54 | * @depends testDeleteDatabase
55 | */
56 | public function testCreateDatabase()
57 | {
58 | $dbName2 = $this->getTestDatabase().'2';
59 | $this->couchClient->deleteDatabase($dbName2);
60 | $this->couchClient->createDatabase($dbName2);
61 |
62 | $dbs = $this->couchClient->getAllDatabases();
63 | $this->assertContains($dbName2, $dbs);
64 |
65 | // Tidy
66 | $this->couchClient->deleteDatabase($dbName2);
67 | }
68 |
69 | public function testDropMultipleTimesSkips()
70 | {
71 | $this->couchClient->deleteDatabase($this->getTestDatabase());
72 | $this->couchClient->deleteDatabase($this->getTestDatabase());
73 | }
74 |
75 | /**
76 | * @depends testCreateDatabase
77 | */
78 | public function testCreateDuplicateDatabaseThrowsException()
79 | {
80 | $this->setExpectedException('Doctrine\CouchDB\HTTP\HTTPException', 'HTTP Error with status 412 occurred while requesting /'.$this->getTestDatabase().'. Error: file_exists The database could not be created, the file already exists.');
81 | $this->couchClient->createDatabase($this->getTestDatabase());
82 | }
83 |
84 | public function testGetDatabaseInfo()
85 | {
86 | $data = $this->couchClient->getDatabaseInfo($this->getTestDatabase());
87 |
88 | $this->assertInternalType('array', $data);
89 | $this->assertArrayHasKey('db_name', $data);
90 | $this->assertEquals($this->getTestDatabase(), $data['db_name']);
91 |
92 | $notExistedDb = 'not_existed_db';
93 |
94 | $this->setExpectedException('Doctrine\CouchDB\HTTP\HTTPException', 'HTTP Error with status 404 occurred while requesting /'.$notExistedDb.'. Error: not_found Database does not exist');
95 |
96 | $this->couchClient->getDatabaseInfo($notExistedDb);
97 | }
98 |
99 | public function testCreateBulkUpdater()
100 | {
101 | $updater = $this->couchClient->createBulkUpdater();
102 | $this->assertInstanceOf('Doctrine\CouchDB\Utils\BulkUpdater', $updater);
103 | }
104 |
105 | /**
106 | * @depends testCreateBulkUpdater
107 | */
108 | public function testGetChanges()
109 | {
110 | if (version_compare($this->couchClient->getVersion(), '2.0.0') >= 0){
111 | $this->markTestSkipped(
112 | 'This test will not pass on version 2.0.0 due a result order bug. https://github.com/apache/couchdb/issues/513'
113 | );
114 | }
115 |
116 | $updater = $this->couchClient->createBulkUpdater();
117 | $updater->updateDocument(['_id' => 'test1', 'foo' => 'bar']);
118 | $updater->updateDocument(['_id' => 'test2', 'bar' => 'baz']);
119 | $updater->execute();
120 |
121 | $changes = $this->couchClient->getChanges();
122 |
123 | $this->assertArrayHasKey('results', $changes);
124 |
125 | $this->assertEquals(2, count($changes['results']));
126 | $this->assertStringStartsWith('2', $changes['last_seq']);
127 |
128 | // Check the doc_ids parameter.
129 | $changes = $this->couchClient->getChanges([
130 | 'doc_ids' => ['test1'],
131 | ]);
132 |
133 | $this->assertArrayHasKey('results', $changes);
134 | $this->assertEquals(1, count($changes['results']));
135 | $this->assertArrayHasKey('id', $changes['results'][0]);
136 | $this->assertEquals('test1', $changes['results'][0]['id']);
137 | $this->assertStringStartsWith('2', $changes['last_seq']);
138 |
139 | $changes = $this->couchClient->getChanges([
140 | 'doc_ids' => null,
141 | ]);
142 | $this->assertArrayHasKey('results', $changes);
143 | $this->assertEquals(2, count($changes['results']));
144 | $this->assertStringStartsWith('2', $changes['last_seq']);
145 |
146 | // Check the limit parameter.
147 | $changes = $this->couchClient->getChanges([
148 | 'limit' => 1,
149 | ]);
150 |
151 | $this->assertArrayHasKey('results', $changes);
152 | $this->assertEquals(1, count($changes['results']));
153 | $this->assertStringStartsWith('1', $changes['last_seq']);
154 |
155 | // Checks the descending parameter.
156 | $changes = $this->couchClient->getChanges([
157 | 'descending' => true,
158 | ]);
159 |
160 | $this->assertArrayHasKey('results', $changes);
161 | $this->assertEquals(2, count($changes['results']));
162 | $this->assertStringStartsWith('1', $changes['last_seq']);
163 |
164 | // Checks the since parameter.
165 | $changes = $this->couchClient->getChanges([
166 | 'since' => 1,
167 | ]);
168 |
169 | $this->assertArrayHasKey('results', $changes);
170 | $this->assertEquals(1, count($changes['results']));
171 | $this->assertStringStartsWith('2', $changes['last_seq']);
172 |
173 | // Checks the filter parameter.
174 | $designDocPath = __DIR__.'/../../Models/CMS/_files';
175 |
176 | // Create a filter, that filters the only doc with {"_id":"test1"}
177 | $client = $this->couchClient;
178 | $client->createDesignDocument('test-filter', new FolderDesignDocument($designDocPath));
179 |
180 | $changes = $this->couchClient->getChanges([
181 | 'filter' => 'test-filter/my_filter',
182 | ]);
183 | $this->assertEquals(1, count($changes['results']));
184 | $this->assertStringStartsWith('3', $changes['last_seq']);
185 | }
186 |
187 | public function testPostDocument()
188 | {
189 | $client = $this->couchClient;
190 | list($id, $rev) = $client->postDocument(['foo' => 'bar']);
191 |
192 | $response = $client->findDocument($id);
193 | $this->assertEquals(['_id' => $id, '_rev' => $rev, 'foo' => 'bar'], $response->body);
194 | }
195 |
196 | public function testPutDocument()
197 | {
198 | $id = 'foo-bar-baz';
199 | $client = $this->couchClient;
200 | list($id, $rev) = $client->putDocument(['foo' => 'bar'], $id);
201 |
202 | $response = $client->findDocument($id);
203 | $this->assertEquals(['_id' => $id, '_rev' => $rev, 'foo' => 'bar'], $response->body);
204 |
205 | list($id, $rev) = $client->putDocument(['foo' => 'baz'], $id, $rev);
206 |
207 | $response = $client->findDocument($id);
208 | $this->assertEquals(['_id' => $id, '_rev' => $rev, 'foo' => 'baz'], $response->body);
209 | }
210 |
211 | public function testDeleteDocument()
212 | {
213 | $client = $this->couchClient;
214 | list($id, $rev) = $client->postDocument(['foo' => 'bar']);
215 |
216 | $client->deleteDocument($id, $rev);
217 |
218 | $response = $client->findDocument($id);
219 | $this->assertEquals(404, $response->status);
220 | }
221 |
222 | public function testCreateDesignDocument()
223 | {
224 | $designDocPath = __DIR__.'/../../Models/CMS/_files';
225 |
226 | $client = $this->couchClient;
227 | $client->createDesignDocument('test-design-doc-create', new FolderDesignDocument($designDocPath));
228 |
229 | $response = $client->findDocument('_design/test-design-doc-create');
230 | $this->assertEquals(200, $response->status);
231 | }
232 |
233 | public function testCreateViewQuery()
234 | {
235 | $designDocPath = __DIR__.'/../../Models/CMS/_files';
236 |
237 | $client = $this->couchClient;
238 | $designDoc = new FolderDesignDocument($designDocPath);
239 |
240 | $query = $client->createViewQuery('test-design-doc-query', 'username', $designDoc);
241 | $this->assertInstanceOf('Doctrine\CouchDB\View\Query', $query);
242 |
243 | $result = $query->execute();
244 |
245 | $this->assertInstanceOf('Doctrine\CouchDB\View\Result', $result);
246 | $this->assertEquals(0, $result->getOffset());
247 | $this->assertEquals(0, $result->getTotalRows());
248 | $this->assertEquals(0, count($result));
249 | }
250 |
251 | public function testQueryWithKeys()
252 | {
253 | $designDocPath = __DIR__.'/../../Models/CMS/_files';
254 |
255 | $client = $this->couchClient;
256 | $ids = [];
257 | for ($i = 0; $i < 10; $i++) {
258 | $data = [
259 | 'type' => 'Doctrine.Tests.Models.CMS.CmsUser',
260 | 'username' => "user-$i",
261 | ];
262 | list($id, $rev) = $client->putDocument($data, "query-with-key-$i");
263 | $ids[] = $id;
264 | }
265 |
266 | $designDoc = new FolderDesignDocument($designDocPath);
267 |
268 | $query = $client->createViewQuery('test-design-doc-query', 'username', $designDoc);
269 | $query->setKeys($ids);
270 |
271 | $this->assertInstanceOf('Doctrine\CouchDB\View\Query', $query);
272 |
273 | $result = $query->execute();
274 |
275 | $this->assertEquals(10, $result->getTotalRows());
276 | }
277 |
278 | public function testCompactDatabase()
279 | {
280 | $client = $this->couchClient;
281 | $client->compactDatabase();
282 | }
283 |
284 | public function testCompactView()
285 | {
286 | $client = $this->couchClient;
287 |
288 | $designDocPath = __DIR__.'/../../Models/CMS/_files';
289 |
290 | $client = $this->couchClient;
291 | $designDoc = new FolderDesignDocument($designDocPath);
292 |
293 | $query = $client->createViewQuery('test-design-doc-query', 'username', $designDoc);
294 | $result = $query->execute();
295 |
296 | $client->compactView('test-design-doc-query');
297 | }
298 |
299 | public function testFindDocument()
300 | {
301 | $client = $this->couchClient;
302 | // Test fetching of document.
303 | list($id, $rev) = $client->postDocument(['foo' => 'bar']);
304 | $response = $client->findDocument($id);
305 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
306 | $this->assertObjectHasAttribute('body', $response);
307 | $body = $response->body;
308 | $this->assertEquals(
309 | ['_id' => $id, '_rev' => $rev, 'foo' => 'bar'],
310 | $body
311 | );
312 | }
313 |
314 | /**
315 | * @depends testCreateBulkUpdater
316 | */
317 | public function testFindRevisions()
318 | {
319 | $client = $this->couchClient;
320 |
321 | // The _id of all the documents is same. So we will get multiple leaf
322 | // revisions.
323 | $id = 'multiple_revisions';
324 | $docs = [
325 | ['_id' => $id, '_rev' => '1-abc', 'foo' => 'bar1'],
326 | ['_id' => $id, '_rev' => '1-bcd', 'foo' => 'bar2'],
327 | ['_id' => $id, '_rev' => '1-cde', 'foo' => 'bar3'],
328 | ];
329 |
330 | // Add the documents to the test db using Bulk API.
331 | $updater = $this->couchClient->createBulkUpdater();
332 | $updater->updateDocuments($docs);
333 | // Set newedits to false to use the supplied _rev instead of assigning
334 | // new ones.
335 | $updater->setNewEdits(false);
336 | $response = $updater->execute();
337 |
338 | // Test fetching of documents of all revisions. By default all
339 | // revisions are fetched.
340 | $response = $client->findRevisions($id);
341 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
342 | $this->assertObjectHasAttribute('body', $response);
343 | $expected = [
344 | ['ok' => $docs[0]],
345 | ['ok' => $docs[1]],
346 | ['ok' => $docs[2]],
347 | ];
348 |
349 | $this->assertEquals($expected, $response->body);
350 | // Test fetching of specific revisions.
351 | $response = $client->findRevisions(
352 | $id,
353 | ['1-abc', '1-cde', '100-ghfgf', '200-blah']
354 | );
355 |
356 | $this->assertInstanceOf('\Doctrine\CouchDB\HTTP\Response', $response);
357 | $this->assertObjectHasAttribute('body', $response);
358 |
359 | $body = $response->body;
360 | $this->assertEquals(4, count($body));
361 | // Doc with _rev = 1-abc.
362 | $this->assertEquals($docs[0], $body[0]['ok']);
363 | // Doc with _rev = 1-cde.
364 | $this->assertEquals($docs[2], $body[1]['ok']);
365 |
366 | // Missing revisions.
367 | $this->assertEquals(['missing' => '100-ghfgf'], $body[2]);
368 | $this->assertEquals(['missing' => '200-blah'], $body[3]);
369 | }
370 |
371 | public function testFindDocuments()
372 | {
373 | $client = $this->couchClient;
374 |
375 | $ids = [];
376 | $expectedRows = [];
377 | foreach (range(1, 3) as $i) {
378 | list($id, $rev) = $client->postDocument(['foo' => 'bar'.$i]);
379 | $ids[] = $id;
380 | // This structure might be dependent from couchdb version. Tested against v2.0.0
381 | $expectedRows[] = [
382 | 'id' => $id,
383 | 'key' => $id,
384 | 'value' => [
385 | 'rev' => $rev,
386 | ],
387 | 'doc' => [
388 | '_id' => $id,
389 | '_rev' => $rev,
390 | 'foo' => 'bar'.$i,
391 | ],
392 | ];
393 | }
394 |
395 | $response = $client->findDocuments($ids);
396 |
397 | $this->assertEquals(['total_rows' => 3, 'rows' => $expectedRows], $response->body);
398 |
399 | $response = $client->findDocuments($ids, 0);
400 | $this->assertEquals(['total_rows' => 3, 'rows' => $expectedRows], $response->body);
401 |
402 | $response = $client->findDocuments($ids, 1);
403 | $this->assertEquals(['total_rows' => 3, 'rows' => [$expectedRows[0]]], $response->body);
404 |
405 | $response = $client->findDocuments($ids, 0, 2);
406 | $this->assertEquals(['total_rows' => 3, 'rows' => [$expectedRows[2]]], $response->body);
407 |
408 | $response = $client->findDocuments($ids, 1, 1);
409 | $this->assertEquals(['total_rows' => 3, 'rows' => [$expectedRows[1]]], $response->body);
410 | }
411 |
412 | public function testAllDocs()
413 | {
414 | $client = $this->couchClient;
415 |
416 | $ids = [];
417 | $expectedRows = [];
418 | foreach (range(1, 3) as $i) {
419 | list($id, $rev) = $client->postDocument(['foo' => 'bar'.$i]);
420 | $ids[] = $id;
421 | // This structure might be dependent from couchdb version. Tested against v2.0.0
422 | $expectedRows[] = [
423 | 'id' => $id,
424 | 'value' => [
425 | 'rev' => $rev,
426 | ],
427 | 'doc' => [
428 | '_id' => $id,
429 | '_rev' => $rev,
430 | 'foo' => 'bar'.$i,
431 | ],
432 | 'key' => $id,
433 | ];
434 | }
435 |
436 | // Everything
437 | $response = $client->allDocs();
438 | $this->assertEquals(['total_rows' => 3, 'offset' => 0, 'rows' => $expectedRows], $response->body);
439 |
440 | // No Limit
441 | $response = $client->allDocs(0);
442 | $this->assertEquals(['total_rows' => 3, 'offset' => 0, 'rows' => $expectedRows], $response->body);
443 |
444 | // Limit
445 | $response = $client->allDocs(1);
446 | $this->assertEquals(['total_rows' => 3, 'offset' => 0, 'rows' => [$expectedRows[0]]], $response->body);
447 |
448 | // Limit
449 | $response = $client->allDocs(2);
450 | $this->assertEquals(['total_rows' => 3, 'offset' => 0, 'rows' => [$expectedRows[0], $expectedRows[1]]], $response->body);
451 |
452 | // Start Key
453 | $response = $client->allDocs(0, $ids[1]);
454 | $this->assertEquals(['total_rows' => 3, 'offset' => 1, 'rows' => [$expectedRows[1], $expectedRows[2]]], $response->body);
455 |
456 | // Start Key with Limit
457 | $response = $client->allDocs(1, $ids[2]);
458 | $this->assertEquals(['total_rows' => 3, 'offset' => 2, 'rows' => [$expectedRows[2]]], $response->body);
459 |
460 | // End key
461 | $response = $client->allDocs(0, null, $ids[0]);
462 | $this->assertEquals(['total_rows' => 3, 'offset' => 0, 'rows' => [$expectedRows[0]]], $response->body);
463 |
464 | // Skip
465 | $response = $client->allDocs(0, null, null, 1);
466 | $this->assertEquals(['total_rows' => 3, 'offset' => 1, 'rows' => [$expectedRows[1], $expectedRows[2]]], $response->body);
467 |
468 | // Skip, Descending
469 | $response = $client->allDocs(null, null, null, 1, true);
470 | $this->assertEquals(['total_rows' => 3, 'offset' => 1, 'rows' => [$expectedRows[1], $expectedRows[0]]], $response->body);
471 |
472 | // Limit, Descending
473 | $response = $client->allDocs(1, null, null, null, true);
474 | $this->assertEquals(['total_rows' => 3, 'offset' => 0, 'rows' => [$expectedRows[2]]], $response->body);
475 |
476 | // tidy
477 | $client->deleteDocument($expectedRows[0]['id'], $expectedRows[0]['value']['rev']);
478 | $client->deleteDocument($expectedRows[1]['id'], $expectedRows[1]['value']['rev']);
479 | $client->deleteDocument($expectedRows[2]['id'], $expectedRows[2]['value']['rev']);
480 | }
481 |
482 | public function testGetActiveTasks()
483 | {
484 | $client = $this->couchClient;
485 | $active_tasks = $client->getActiveTasks();
486 | $this->assertEquals([], $active_tasks);
487 |
488 | $sourceDatabase = $this->getTestDatabase();
489 | $targetDatabase1 = $this->getTestDatabase().'target1';
490 | $targetDatabase2 = $this->getTestDatabase().'target2';
491 | $this->couchClient->deleteDatabase($targetDatabase1);
492 | $this->couchClient->deleteDatabase($targetDatabase2);
493 | $this->couchClient->createDatabase($targetDatabase1);
494 | $this->couchClient->createDatabase($targetDatabase2);
495 |
496 | $client->replicate($sourceDatabase, $targetDatabase1, null, true);
497 | //Receiving empty array when requesting straight away
498 | sleep(5);
499 | $active_tasks = $client->getActiveTasks(true);
500 |
501 | $this->assertTrue(count($active_tasks) == 1);
502 |
503 | $client->replicate($sourceDatabase, $targetDatabase2, null, true);
504 | sleep(5);
505 | $active_tasks = $client->getActiveTasks();
506 | $this->assertTrue(count($active_tasks) == 2);
507 |
508 | $client->replicate($sourceDatabase, $targetDatabase1, true, true);
509 | $client->replicate($sourceDatabase, $targetDatabase2, true, true);
510 |
511 | sleep(5);
512 | $active_tasks = $client->getActiveTasks();
513 | $this->assertEquals([], $active_tasks);
514 |
515 | // Tidy
516 | $this->couchClient->deleteDatabase($targetDatabase1);
517 | $this->couchClient->deleteDatabase($targetDatabase2);
518 | }
519 |
520 | public function testGetRevisionDifference()
521 | {
522 | $client = $this->couchClient;
523 | $mapping = [
524 | 'baz' => [
525 | 0 => '2-7051cbe5c8faecd085a3fa619e6e6337',
526 | ],
527 | 'foo' => [
528 | 0 => '3-6a540f3d701ac518d3b9733d673c5484',
529 | ],
530 | 'bar' => [
531 | 0 => '1-d4e501ab47de6b2000fc8a02f84a0c77',
532 | 1 => '1-967a00dff5e02add41819138abb3284d',
533 | ],
534 | ];
535 | $revisionDifference = [
536 | 'baz' => [
537 | 'missing' => [
538 | 0 => '2-7051cbe5c8faecd085a3fa619e6e6337',
539 | ],
540 | ],
541 | 'foo' => [
542 | 'missing' => [
543 | 0 => '3-6a540f3d701ac518d3b9733d673c5484',
544 | ],
545 | ],
546 | 'bar' => [
547 | 'missing' => [
548 | 0 => '1-d4e501ab47de6b2000fc8a02f84a0c77',
549 | 1 => '1-967a00dff5e02add41819138abb3284d',
550 | ],
551 | ],
552 | ];
553 |
554 | list($id, $rev) = $client->putDocument(['name' => 'test'], 'foo');
555 | $mapping['foo'][] = $rev;
556 | $revDiff = $client->getRevisionDifference($mapping);
557 | if (isset($revDiff['foo']['possible_ancestors'])) {
558 | $revisionDifference['foo']['possible_ancestors'] = $revDiff['foo']['possible_ancestors'];
559 | }
560 | $this->assertEquals($revisionDifference, $revDiff);
561 | }
562 |
563 | /**
564 | * @depends testCreateBulkUpdater
565 | */
566 | public function testTransferChangedDocuments()
567 | {
568 | $client = $this->couchClient;
569 |
570 | // Doc id.
571 | $id = 'multiple_attachments';
572 | // Document with attachments.
573 | $docWithAttachment = [
574 | '_id' => $id,
575 | '_rev' => '1-abc',
576 | '_attachments' => [
577 | 'foo.txt' => [
578 | 'content_type' => 'text/plain',
579 | 'data' => 'VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=',
580 | ],
581 | 'bar.txt' => [
582 | 'content_type' => 'text/plain',
583 | 'data' => 'VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=',
584 | ],
585 | ],
586 | ];
587 | // Doc without any attachment. The id of both the docs is same.
588 | // So we will get two leaf revisions.
589 | $doc = ['_id' => $id, 'foo' => 'bar', '_rev' => '1-bcd'];
590 |
591 | // Add the documents to the test db using Bulk API.
592 | $updater = $this->couchClient->createBulkUpdater();
593 | $updater->updateDocument($docWithAttachment);
594 | $updater->updateDocument($doc);
595 | // Set newedits to false to use the supplied _rev instead of assigning
596 | // new ones.
597 | $updater->setNewEdits(false);
598 | $response = $updater->execute();
599 |
600 | // Create the copy database and a copyClient to interact with it.
601 | $copyDb = $this->getTestDatabase().'_copy';
602 |
603 | $this->couchClient->deleteDatabase($copyDb);
604 | sleep(0.5);
605 | $this->couchClient->createDatabase($copyDb);
606 |
607 | $copyClient = new CouchDBClient($client->getHttpClient(), $copyDb);
608 |
609 | // Missing revisions in the $copyDb.
610 | $missingRevs = ['1-abc', '1-bcd'];
611 | // Transfer the missing revisions from the source to the target.
612 |
613 | $response = $client->transferChangedDocuments($id, $missingRevs, $copyClient, true);
614 |
615 | list($docStack, $responses) = $client->transferChangedDocuments($id, $missingRevs, $copyClient);
616 | // $docStack should contain the doc that didn't have the attachment.
617 | $this->assertEquals(1, count($docStack));
618 | $this->assertEquals($doc, json_decode($docStack[0], true));
619 |
620 | // The doc with attachment should have been copied to the copyDb.
621 | $this->assertEquals(1, count($responses));
622 | $this->assertArrayHasKey('ok', $responses[0]);
623 | $this->assertEquals(true, $responses[0]['ok']);
624 | $client->deleteDatabase($copyDb);
625 | }
626 |
627 | /**
628 | * @depends testGetChanges
629 | */
630 | public function testGetChangesAsStream()
631 | {
632 | $client = $this->couchClient;
633 |
634 | // Stream of changes feed.
635 | $stream = $client->getChangesAsStream();
636 | list($id, $rev) = $client->postDocument(['_id' => 'stream1', 'foo' => 'bar']);
637 | // Get the change feed data for stream1.
638 | while (trim($line = fgets($stream)) == '');
639 | $this->assertEquals('stream1', json_decode($line, true)['id']);
640 | list($id, $rev) = $client->postDocument(['_id' => 'stream2', 'foo' => 'bar']);
641 | // Get the change feed data for stream2.
642 | while (trim($line = fgets($stream)) == '');
643 | $this->assertEquals('stream2', json_decode($line, true)['id']);
644 | fclose($stream);
645 | }
646 |
647 | public function testEnsureFullCommit()
648 | {
649 | $client = $this->couchClient;
650 | $body = $client->ensureFullCommit();
651 | $this->assertArrayHasKey('instance_start_time', $body);
652 | $this->assertArrayHasKey('ok', $body);
653 | $this->assertEquals(true, $body['ok']);
654 | }
655 |
656 | public function test404WhenQueryAndNoDesignDocument()
657 | {
658 | $client = $this->couchClient;
659 | $query = $client->createViewQuery('foo', 'not-found');
660 |
661 | $this->setExpectedException(
662 | 'Doctrine\CouchDB\HTTP\HTTPException',
663 | 'HTTP Error with status 404 occurred while requesting /doctrine_test_database/_design/foo/_view/not-found?. Error: not_found missing'
664 | );
665 |
666 | $query->execute();
667 | }
668 |
669 | public function testEncodeQueryParamsCorrectly()
670 | {
671 | $designDocPath = __DIR__.'/../../Models/CMS/_files';
672 |
673 | $client = $this->couchClient;
674 | $designDoc = new FolderDesignDocument($designDocPath);
675 |
676 | $query = $client->createViewQuery('test-design-doc-query', 'username', $designDoc);
677 | $query->setStartKey(['foo', 'bar']);
678 | $query->setEndKey(['bar', 'baz']);
679 | $query->setStale(true);
680 | $query->setDescending(true);
681 |
682 | $result = $query->execute();
683 |
684 | $this->assertEquals(0, $result->getTotalRows());
685 | }
686 | }
687 |
--------------------------------------------------------------------------------