3 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15 * This software consists of voluntary contributions made by many individuals
16 * and is licensed under the MIT license. For more information, see
17 * <http://www.doctrine-project.org>.
20 namespace Doctrine\CouchDB
;
22 use Doctrine\CouchDB\HTTP\Client
;
23 use Doctrine\CouchDB\HTTP\Response
;
24 use Doctrine\CouchDB\HTTP\HTTPException
;
25 use Doctrine\CouchDB\HTTP\MultipartParserAndSender
;
26 use Doctrine\CouchDB\HTTP\StreamClient
;
27 use Doctrine\CouchDB\Utils\BulkUpdater
;
28 use Doctrine\CouchDB\View\DesignDocument
;
31 * CouchDB client class
33 * @license http://www.opensource.org/licenses/mit-license.php MIT
34 * @link www.doctrine-project.com
36 * @author Benjamin Eberlei <kontakt@beberlei.de>
37 * @author Lukas Kahwe Smith <smith@pooteeweet.org>
42 const COLLATION_END
= "\xEF\xBF\xB0";
45 * Name of the CouchDB database
49 protected $databaseName;
52 * The underlying HTTP Connection of the used DocumentManager.
63 private $version = null;
65 static private $clients = array(
66 'socket' => 'Doctrine\CouchDB\HTTP\SocketClient',
67 'stream' => 'Doctrine\CouchDB\HTTP\StreamClient',
71 * Factory method for CouchDBClients
73 * @param array $options
74 * @return CouchDBClient
75 * @throws \InvalidArgumentException
77 static public function create(array $options)
79 if (isset($options['url'])) {
80 $urlParts = parse_url($options['url']);
81 $urlOptions = ['user' => 'user', 'pass' => 'password', 'host' => 'host', 'path' => 'dbname', 'port' => 'port'];
82 foreach ($urlParts as $part => $value) {
87 $options[$part] = $value;
91 $path = explode('/', $value);
92 $options['dbname'] = array_pop($path);
93 $options['path'] = trim(implode('/', $path), '/');
97 $options['password'] = $value;
101 $options['ssl'] = ($value === 'https');
110 if (!isset($options['dbname'])) {
111 throw new \
InvalidArgumentException("'dbname' is a required option to create a CouchDBClient");
116 'host' => 'localhost',
126 $options = array_merge($defaults, $options);
128 if (!isset(self
::$clients[$options['type']])) {
129 throw new \
InvalidArgumentException(sprintf('There is no client implementation registered for %s, valid options are: %s',
130 $options['type'], implode(", ", array_keys(self
::$clients))
133 $connectionClass = self
::$clients[$options['type']];
134 $connection = new $connectionClass(
138 $options['password'],
144 if ($options['logging'] === true) {
145 $connection = new HTTP\
LoggingClient($connection);
147 return new static($connection, $options['dbname']);
151 * @param Client $client
152 * @param string $databaseName
154 public function __construct(Client
$client, $databaseName)
156 $this->httpClient
= $client;
157 $this->databaseName
= $databaseName;
160 public function setHttpClient(Client
$client)
162 $this->httpClient
= $client;
168 public function getHttpClient()
170 return $this->httpClient
;
173 public function getDatabase()
175 return $this->databaseName
;
179 * Let CouchDB generate an array of UUIDs.
183 * @throws CouchDBException
185 public function getUuids($count = 1)
187 $count = (int)$count;
188 $response = $this->httpClient
->request('GET', '/_uuids?count=' . $count);
190 if ($response->status
!= 200) {
191 throw new CouchDBException("Could not retrieve UUIDs from CouchDB.");
194 return $response->body
['uuids'];
198 * Find a document by ID and return the HTTP response.
201 * @return HTTP\Response
203 public function findDocument($id)
205 $documentPath = '/' . $this->databaseName
. '/' . urlencode($id);
206 return $this->httpClient
->request('GET', $documentPath);
210 * Find documents of all or the specified revisions.
212 * If $revisions is an array containing the revisions to be fetched, only
213 * the documents of those revisions are fetched. Else document of all
214 * leaf revisions are fetched.
216 * @param string $docId
217 * @param mixed $revisions
218 * @return HTTP\Response
220 public function findRevisions($docId, $revisions = null)
222 $path = '/' . $this->databaseName
. '/' . urlencode($docId);
223 if (is_array($revisions)) {
224 // Fetch documents of only the specified leaf revisions.
225 $path .= '?open_revs=' . json_encode($revisions);
227 // Fetch documents of all leaf revisions.
228 $path .= '?open_revs=all';
230 // Set the Accept header to application/json to get a JSON array in the
231 // response's body. Without this the response is multipart/mixed stream.
232 return $this->httpClient
->request(
237 array('Accept' => 'application/json')
242 * Find many documents by passing their ids and return the HTTP response.
246 * @param null $offset
247 * @return HTTP\Response
249 public function findDocuments(array $ids, $limit = null, $offset = null)
251 $allDocsPath = '/' . $this->databaseName
. '/_all_docs?include_docs=true';
253 $allDocsPath .= '&limit=' . (int)$limit;
256 $allDocsPath .= '&skip=' . (int)$offset;
259 return $this->httpClient
->request('POST', $allDocsPath, json_encode(
260 array('keys' => array_values($ids)))
267 * @param int|null $limit
268 * @param string|null $startKey
269 * @param string|null $endKey
270 * @param int|null $skip
271 * @param bool $descending
272 * @return HTTP\Response
274 public function allDocs($limit = null, $startKey = null, $endKey = null, $skip = null, $descending = false)
276 $allDocsPath = '/' . $this->databaseName
. '/_all_docs?include_docs=true';
278 $allDocsPath .= '&limit=' . (int)$limit;
281 $allDocsPath .= '&startkey="' . (string)$startKey.'"';
283 if (!is_null($endKey)) {
284 $allDocsPath .= '&endkey="' . (string)$endKey.'"';
286 if (!is_null($skip) && (int)$skip > 0) {
287 $allDocsPath .= '&skip=' . (int)$skip;
289 if (!is_null($descending) && (bool)$descending === true) {
290 $allDocsPath .= '&descending=true';
292 return $this->httpClient
->request('GET', $allDocsPath);
296 * Get the current version of CouchDB.
298 * @throws HTTPException
301 public function getVersion()
303 if ($this->version
=== null) {
304 $response = $this->httpClient
->request('GET', '/');
305 if ($response->status
!= 200) {
306 throw HTTPException
::fromResponse('/', $response);
309 $this->version
= $response->body
['version'];
311 return $this->version
;
317 * @throws HTTPException
320 public function getAllDatabases()
322 $response = $this->httpClient
->request('GET', '/_all_dbs');
323 if ($response->status
!= 200) {
324 throw HTTPException
::fromResponse('/_all_dbs', $response);
327 return $response->body
;
331 * Create a new database
333 * @throws HTTPException
334 * @param string $name
337 public function createDatabase($name)
339 $response = $this->httpClient
->request('PUT', '/' . urlencode($name));
341 if ($response->status
!= 201) {
342 throw HTTPException
::fromResponse('/' . urlencode($name), $response);
349 * @throws HTTPException
350 * @param string $name
353 public function deleteDatabase($name)
355 $response = $this->httpClient
->request('DELETE', '/' . urlencode($name));
357 if ($response->status
!= 200 && $response->status
!= 404) {
358 throw HTTPException
::fromResponse('/' . urlencode($name), $response);
363 * Get Information about a database.
365 * @param string $name
367 * @throws HTTPException
369 public function getDatabaseInfo($name = null)
371 $response = $this->httpClient
->request('GET', '/' . ($name ?
urlencode($name) : $this->databaseName
));
373 if ($response->status
!= 200) {
374 throw HTTPException
::fromResponse('/' . urlencode($name), $response);
377 return $response->body
;
383 * @param array $params
385 * @throws HTTPException
387 public function getChanges(array $params = array())
389 $path = '/' . $this->databaseName
. '/_changes';
391 $method = ((!isset($params['doc_ids']) ||
$params['doc_ids'] == null) ?
"GET" : "POST");
394 if ($method == "GET") {
396 foreach ($params as $key => $value) {
397 if (isset($params[$key]) === true && is_bool($value) === true) {
398 $params[$key] = ($value) ?
'true' : 'false';
401 if (count($params) > 0) {
402 $query = http_build_query($params);
403 $path = $path.'?'.$query;
405 $response = $this->httpClient
->request('GET', $path);
408 $path .= '?filter=_doc_ids';
409 $response = $this->httpClient
->request('POST', $path, json_encode($params));
411 if ($response->status
!= 200) {
412 throw HTTPException
::fromResponse($path, $response);
415 return $response->body
;
419 * Create a bulk updater instance.
421 * @return BulkUpdater
423 public function createBulkUpdater()
425 return new BulkUpdater($this->httpClient
, $this->databaseName
);
429 * Execute a POST request against CouchDB inserting a new document, leaving the server to generate a uuid.
432 * @return array<id, rev>
433 * @throws HTTPException
435 public function postDocument(array $data)
437 $path = '/' . $this->databaseName
;
438 $response = $this->httpClient
->request('POST', $path, json_encode($data));
440 if ($response->status
!= 201) {
441 throw HTTPException
::fromResponse($path, $response);
444 return array($response->body
['id'], $response->body
['rev']);
448 * Execute a PUT request against CouchDB inserting or updating a document.
452 * @param string|null $rev
453 * @return array<id, rev>
454 * @throws HTTPException
456 public function putDocument($data, $id, $rev = null)
460 $data['_rev'] = $rev;
463 $path = '/' . $this->databaseName
. '/' . urlencode($id);
464 $response = $this->httpClient
->request('PUT', $path, json_encode($data));
466 if ($response->status
!= 201) {
467 throw HTTPException
::fromResponse($path, $response);
470 return array($response->body
['id'], $response->body
['rev']);
479 * @throws HTTPException
481 public function deleteDocument($id, $rev)
483 $path = '/' . $this->databaseName
. '/' . urlencode($id) . '?rev=' . $rev;
484 $response = $this->httpClient
->request('DELETE', $path);
486 if ($response->status
!= 200) {
487 throw HTTPException
::fromResponse($path, $response);
492 * @param string $designDocName
493 * @param string $viewName
494 * @param DesignDocument $designDoc
497 public function createViewQuery($designDocName, $viewName, DesignDocument
$designDoc = null)
499 return new View\
Query($this->httpClient
, $this->databaseName
, $designDocName, $viewName, $designDoc);
503 * Create or update a design document from the given in memory definition.
505 * @param string $designDocName
506 * @param DesignDocument $designDoc
507 * @return HTTP\Response
509 public function createDesignDocument($designDocName, DesignDocument
$designDoc)
511 $data = $designDoc->getData();
512 $data['_id'] = '_design/' . $designDocName;
514 $documentPath = '/' . $this->databaseName
. '/' . $data['_id'];
515 $response = $this->httpClient
->request( 'GET', $documentPath );
517 if ($response->status
== 200) {
518 $docData = $response->body
;
519 $data['_rev'] = $docData['_rev'];
522 return $this->httpClient
->request(
524 sprintf("/%s/_design/%s", $this->databaseName
, $designDocName),
532 * Return array of data about compaction status.
535 * @throws HTTPException
537 public function getCompactInfo()
539 $path = sprintf('/%s/_compact', $this->databaseName
);
540 $response = $this->httpClient
->request('GET', $path);
541 if ($response->status
>= 400) {
542 throw HTTPException
::fromResponse($path, $response);
544 return $response->body
;
551 * @throws HTTPException
553 public function compactDatabase()
555 $path = sprintf('/%s/_compact', $this->databaseName
);
556 $response = $this->httpClient
->request('POST', $path);
557 if ($response->status
>= 400) {
558 throw HTTPException
::fromResponse($path, $response);
560 return $response->body
;
564 * POST /db/_compact/designDoc
566 * @param string $designDoc
568 * @throws HTTPException
570 public function compactView($designDoc)
572 $path = sprintf('/%s/_compact/%s', $this->databaseName
, $designDoc);
573 $response = $this->httpClient
->request('POST', $path);
574 if ($response->status
>= 400) {
575 throw HTTPException
::fromResponse($path, $response);
577 return $response->body
;
581 * POST /db/_view_cleanup
584 * @throws HTTPException
586 public function viewCleanup()
588 $path = sprintf('/%s/_view_cleanup', $this->databaseName
);
589 $response = $this->httpClient
->request('POST', $path);
590 if ($response->status
>= 400) {
591 throw HTTPException
::fromResponse($path, $response);
593 return $response->body
;
597 * POST /db/_replicate
599 * @param string $source
600 * @param string $target
601 * @param bool|null $cancel
602 * @param bool|null $continuous
603 * @param string|null $filter
604 * @param array|null $ids
605 * @param string|null $proxy
607 * @throws HTTPException
609 public function replicate($source, $target, $cancel = null, $continuous = null, $filter = null, array $ids = null, $proxy = null)
611 $params = array('target' => $target, 'source' => $source);
612 if ($cancel !== null) {
613 $params['cancel'] = (bool)$cancel;
615 if ($continuous !== null) {
616 $params['continuous'] = (bool)$continuous;
618 if ($filter !== null) {
619 $params['filter'] = $filter;
622 $params['doc_ids'] = $ids;
624 if ($proxy !== null) {
625 $params['proxy'] = $proxy;
627 $path = '/_replicate';
628 $response = $this->httpClient
->request('POST', $path, json_encode($params));
629 if ($response->status
>= 400) {
630 throw HTTPException
::fromResponse($path, $response);
632 return $response->body
;
639 * @throws HTTPException
641 public function getActiveTasks()
643 $response = $this->httpClient
->request('GET', '/_active_tasks');
644 if ($response->status
!= 200) {
645 throw HTTPException
::fromResponse('/_active_tasks', $response);
647 return $response->body
;
651 * Get revision difference.
655 * @throws HTTPException
657 public function getRevisionDifference($data)
659 $path = '/' . $this->databaseName
. '/_revs_diff';
660 $response = $this->httpClient
->request('POST', $path, json_encode($data));
661 if ($response->status
!= 200) {
662 throw HTTPException
::fromResponse($path, $response);
664 return $response->body
;
668 * Transfer missing revisions to the target. The Content-Type of response
669 * from the source should be multipart/mixed.
671 * @param string $docId
672 * @param array $missingRevs
673 * @param CouchDBClient $target
674 * @return array|HTTP\ErrorResponse|string
675 * @throws HTTPException
677 public function transferChangedDocuments($docId, $missingRevs, CouchDBClient
$target)
679 $path = '/' . $this->getDatabase() . '/' . $docId;
680 $params = array('revs' => true, 'latest' => true, 'open_revs' => json_encode($missingRevs));
681 $query = http_build_query($params);
682 $path .= '?' . $query;
684 $targetPath = '/' . $target->getDatabase() . '/' . $docId . '?new_edits=false';
686 $mutltipartHandler = new MultipartParserAndSender($this->getHttpClient(), $target->getHttpClient());
687 return $mutltipartHandler->request(
692 array('Accept' => 'multipart/mixed')
698 * Get changes as a stream.
700 * This method similar to the getChanges() method. But instead of returning
701 * the set of changes, it returns the connection stream from which the response
702 * can be read line by line. This is useful when you want to continuously get changes
703 * as they occur. Filtered changes feed is not supported by this method.
705 * @param array $params
708 * @throws HTTPException
710 public function getChangesAsStream(array $params = array())
712 // Set feed to continuous.
713 if (!isset($params['feed']) ||
$params['feed'] != 'continuous') {
714 $params['feed'] = 'continuous';
716 $path = '/' . $this->databaseName
. '/_changes';
717 $connectionOptions = $this->getHttpClient()->getOptions();
718 $streamClient = new StreamClient(
719 $connectionOptions['host'],
720 $connectionOptions['port'],
721 $connectionOptions['username'],
722 $connectionOptions['password'],
723 $connectionOptions['ip'],
724 $connectionOptions['ssl'],
725 $connectionOptions['path']
728 foreach ($params as $key => $value) {
729 if (isset($params[$key]) === true && is_bool($value) === true) {
730 $params[$key] = ($value) ?
'true' : 'false';
733 if (count($params) > 0) {
734 $query = http_build_query($params);
735 $path = $path . '?' . $query;
737 $stream = $streamClient->getConnection('GET', $path, null);
739 $headers = $streamClient->getStreamHeaders($stream);
740 if (empty($headers['status'])) {
741 throw HTTPException
::readFailure(
742 $connectionOptions['ip'],
743 $connectionOptions['port'],
744 'Received an empty response or not status code',
747 } elseif ($headers['status'] != 200) {
749 while (!feof($stream)) {
750 $body .= fgets($stream);
752 throw HTTPException
::fromResponse($path, new Response($headers['status'], $headers, $body));
754 // Everything seems okay. Return the connection resource.
760 * Commit any recent changes to the specified database to disk.
763 * @throws HTTPException
765 public function ensureFullCommit()
767 $path = '/' . $this->databaseName
. '/_ensure_full_commit';
768 $response = $this->httpClient
->request('POST', $path);
769 if ($response->status
!= 201) {
770 throw HTTPException
::fromResponse($path, $response);
772 return $response->body
;