diff --git a/lib/AlgoliaSearch/Analytics.php b/lib/AlgoliaSearch/Analytics.php new file mode 100644 index 00000000..9b07efbb --- /dev/null +++ b/lib/AlgoliaSearch/Analytics.php @@ -0,0 +1,83 @@ +client = $client; + } + + public function getABTests($params = array()) + { + $params += array('offset' => 0, 'limit' => 10); + + return $this->request('GET', '/2/abtests', $params); + } + + public function getABTest($abTestID) + { + if (!$abTestID) { + throw new AlgoliaException('Cannot retrieve ABTest because the abtestID is invalid.'); + } + + return $this->request('GET', sprintf('/2/abtests/%s', urlencode($abTestID))); + } + + public function addABTest($abTest) + { + return $this->request( + 'POST', + '/2/abtests', + array(), + $abTest + ); + } + + public function stopABTest($abTestID) + { + if (!$abTestID) { + throw new AlgoliaException('Cannot retrieve ABTest because the abtestID is invalid.'); + } + + return $this->request('POST', sprintf('/2/abtests/%s/stop', urlencode($abTestID))); + } + + public function deleteABTest($abTestID) + { + if (!$abTestID) { + throw new AlgoliaException('Cannot retrieve ABTest because the abtestID is invalid.'); + } + + return $this->request('DELETE', sprintf('/2/abtests/%s', urlencode($abTestID))); + } + + public function waitTask($indexName, $taskID, $timeBeforeRetry = 100, $requestHeaders = array()) + { + $this->client->waitTask($indexName, $taskID, $timeBeforeRetry, $requestHeaders); + } + + protected function request( + $method, + $path, + $params = array(), + $data = array() + ) { + return $this->client->request( + $this->client->getContext(), + $method, + $path, + $params, + $data, + array('analytics.algolia.com'), + $this->client->getContext()->connectTimeout, + $this->client->getContext()->readTimeout + ); + } +} diff --git a/lib/AlgoliaSearch/Client.php b/lib/AlgoliaSearch/Client.php index 857d62d9..b721b964 100644 --- a/lib/AlgoliaSearch/Client.php +++ b/lib/AlgoliaSearch/Client.php @@ -97,7 +97,9 @@ public function __construct($applicationID, $apiKey, $hostsArray = null, $option break; case self::FAILING_HOSTS_CACHE: if (! $value instanceof FailingHostsCache) { - throw new \InvalidArgumentException('failingHostsCache must be an instance of \AlgoliaSearch\FailingHostsCache.'); + throw new \InvalidArgumentException( + 'failingHostsCache must be an instance of \AlgoliaSearch\FailingHostsCache.' + ); } break; default: @@ -106,7 +108,13 @@ public function __construct($applicationID, $apiKey, $hostsArray = null, $option } $failingHostsCache = isset($options[self::FAILING_HOSTS_CACHE]) ? $options[self::FAILING_HOSTS_CACHE] : null; - $this->context = new ClientContext($applicationID, $apiKey, $hostsArray, $this->placesEnabled, $failingHostsCache); + $this->context = new ClientContext( + $applicationID, + $apiKey, + $hostsArray, + $this->placesEnabled, + $failingHostsCache + ); } /** @@ -128,15 +136,16 @@ public function __destruct() */ public function setConnectTimeout($connectTimeout, $timeout = 30, $searchTimeout = 5) { - $version = curl_version(); - $isPhpOld = version_compare(phpversion(), '5.2.3', '<'); - $isCurlOld = version_compare($version['version'], '7.16.2', '<'); + if ($connectTimeout < 1) { + $version = curl_version(); - if (($isPhpOld || $isCurlOld) && $this->context->connectTimeout < 1) { - throw new AlgoliaException( - "The timeout can't be a float with a PHP version less than 5.2.3 or a curl version less than 7.16.2" - ); + if (version_compare($version['version'], '7.16.2', '<')) { + throw new AlgoliaException( + "The timeout can't be a float with a curl version less than 7.16.2" + ); + } } + $this->context->connectTimeout = $connectTimeout; $this->context->readTimeout = $timeout; $this->context->searchTimeout = $searchTimeout; @@ -222,12 +231,39 @@ public function setExtraHeader($key, $value) $this->context->setExtraHeader($key, $value); } + public function waitTask($indexName, $taskID, $timeBeforeRetry = 100, $requestHeaders = array()) + { + while (true) { + $res = $this->getTaskStatus($indexName, $taskID, $requestHeaders); + if ($res['status'] === 'published') { + return $res; + } + usleep($timeBeforeRetry * 1000); + } + } + + public function getTaskStatus($indexName, $taskID, $requestHeaders = array()) + { + return $this->request( + $this->context, + 'GET', + sprintf('/1/indexes/%s/task/%s', urlencode($indexName), urlencode($taskID)), + null, + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout, + $requestHeaders + ); + } + /** * This method allows to query multiple indexes with one API call. * * @param array $queries * @param string $indexNameKey * @param string $strategy + * @param array $requestHeaders * * @return mixed * @@ -236,17 +272,20 @@ public function setExtraHeader($key, $value) */ public function multipleQueries($queries, $indexNameKey = 'indexName', $strategy = 'none') { - if ($queries == null) { + $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); + + if ($queries === null) { throw new \Exception('No query provided'); } $requests = array(); foreach ($queries as $query) { - if (array_key_exists($indexNameKey, $query)) { - $indexes = $query[$indexNameKey]; - unset($query[$indexNameKey]); - } else { + if (!array_key_exists($indexNameKey, $query)) { throw new \Exception('indexName is mandatory'); } + + $indexes = $query[$indexNameKey]; + unset($query[$indexNameKey]); + $req = array('indexName' => $indexes, 'params' => $this->buildQuery($query)); array_push($requests, $req); @@ -260,6 +299,33 @@ public function multipleQueries($queries, $indexNameKey = 'indexName', $strategy array('requests' => $requests, 'strategy' => $strategy), $this->context->readHostsArray, $this->context->connectTimeout, + $this->context->searchTimeout, + $requestHeaders + ); + } + + /** + * This method allows to get multiple objects from indexes with one API call. + * + * @param array $queries + * @param string $indexNameKey + * @param string $objectIdKey + * + * @return mixed + * + * @throws AlgoliaException + * @throws \Exception + */ + public function multipleGetObjects($requests, $requestOptions = array()) + { + return $this->request( + $this->context, + 'POST', + '/1/indexes/*/objects', + array(), + array('requests' => $requests), + $this->context->readHostsArray, + $this->context->connectTimeout, $this->context->searchTimeout ); } @@ -280,6 +346,8 @@ public function multipleQueries($queries, $indexNameKey = 'indexName', $strategy */ public function listIndexes() { + $requestHeaders = func_num_args() === 1 && is_array(func_get_arg(0)) ? func_get_arg(0) : array(); + return $this->request( $this->context, 'GET', @@ -288,7 +356,8 @@ public function listIndexes() null, $this->context->readHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -301,6 +370,8 @@ public function listIndexes() */ public function deleteIndex($indexName) { + $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); + return $this->request( $this->context, 'DELETE', @@ -309,7 +380,8 @@ public function deleteIndex($indexName) null, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -324,6 +396,8 @@ public function deleteIndex($indexName) */ public function moveIndex($srcIndexName, $dstIndexName) { + $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); + $request = array('operation' => 'move', 'destination' => $dstIndexName); return $this->request( @@ -334,7 +408,8 @@ public function moveIndex($srcIndexName, $dstIndexName) $request, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -349,7 +424,35 @@ public function moveIndex($srcIndexName, $dstIndexName) */ public function copyIndex($srcIndexName, $dstIndexName) { - $request = array('operation' => 'copy', 'destination' => $dstIndexName); + $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); + + return $this->scopedCopyIndex($srcIndexName, $dstIndexName, array(), $requestHeaders); + } + + /** + * Copy an existing index and define what to copy instead of records: + * - settings + * - synonyms + * - query rules + * + * By default, everything is copied. + * + * @param string $srcIndexName the name of index to copy. + * @param string $dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overwritten if it already exist). + * @param array $scope Resource to copy instead of records: 'settings', 'rules', 'synonyms' + * @param array $requestHeaders + * @return mixed + */ + public function scopedCopyIndex($srcIndexName, $dstIndexName, array $scope = array(), array $requestHeaders = array()) + { + $request = array( + 'operation' => 'copy', + 'destination' => $dstIndexName, + ); + + if (! empty($scope)) { + $request['scope'] = $scope; + } return $this->request( $this->context, @@ -359,7 +462,8 @@ public function copyIndex($srcIndexName, $dstIndexName) $request, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -376,6 +480,8 @@ public function copyIndex($srcIndexName, $dstIndexName) */ public function getLogs($offset = 0, $length = 10, $type = 'all') { + $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); + if (gettype($type) == 'boolean') { //Old prototype onlyError if ($type) { $type = 'error'; @@ -392,7 +498,229 @@ public function getLogs($offset = 0, $length = 10, $type = 'all') null, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders + ); + } + + /** + * Add a userID to the mapping + * @return an object containing a "updatedAt" attribute + * + * @throws AlgoliaException + */ + public function assignUserID($userID, $clusterName) + { + $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); + $requestHeaders["X-Algolia-User-ID"] = $userID; + + $request = array('cluster' => $clusterName); + + return $this->request( + $this->context, + 'POST', + '/1/clusters/mapping', + null, + $request, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout, + $requestHeaders + ); + } + + /** + * Remove a userID from the mapping + * @return an object containing a "deletedAt" attribute + * + * @throws AlgoliaException + */ + public function removeUserID($userID) + { + $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); + $requestHeaders["X-Algolia-User-ID"] = $userID; + + return $this->request( + $this->context, + 'DELETE', + '/1/clusters/mapping', + null, + null, + $this->context->writeHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout, + $requestHeaders + ); + } + + /** + * List available cluster in the mapping + * return an object in the form: + * array( + * "clusters" => array( + * array("clusterName" => "name", "nbRecords" => 0, "nbUserIDs" => 0, "dataSize" => 0) + * ) + * ). + * + * @return mixed + * @throws AlgoliaException + */ + public function listClusters() + { + $requestHeaders = func_num_args() === 1 && is_array(func_get_arg(0)) ? func_get_arg(0) : array(); + + return $this->request( + $this->context, + 'GET', + '/1/clusters', + null, + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout, + $requestHeaders + ); + } + + /** + * Get one userID in the mapping + * return an object in the form: + * array( + * "userID" => "userName", + * "clusterName" => "name", + * "nbRecords" => 0, + * "dataSize" => 0 + * ). + * + * @return mixed + * @throws AlgoliaException + */ + public function getUserID($userID) + { + $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); + + return $this->request( + $this->context, + 'GET', + '/1/clusters/mapping/'.urlencode($userID), + null, + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout, + $requestHeaders + ); + } + + /** + * List userIDs in the mapping + * return an object in the form: + * array( + * "userIDs" => array( + * array("userID" => "userName", "clusterName" => "name", "nbRecords" => 0, "dataSize" => 0) + * ), + * "page" => 0, + * "hitsPerPage" => 20 + * ). + * + * @return mixed + * @throws AlgoliaException + */ + public function listUserIDs($page = 0, $hitsPerPage = 20) + { + $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); + + return $this->request( + $this->context, + 'GET', + '/1/clusters/mapping?page='.$page.'&hitsPerPage='.$hitsPerPage, + null, + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout, + $requestHeaders + ); + } + + /** + * Get top userID in the mapping + * return an object in the form: + * array( + * "topUsers" => array( + * "clusterName" => array( + * array("userID" => "userName", "nbRecords" => 0, "dataSize" => 0) + * ) + * ) + * ). + * + * @return mixed + * @throws AlgoliaException + */ + public function getTopUserID() + { + $requestHeaders = func_num_args() === 1 && is_array(func_get_arg(0)) ? func_get_arg(0) : array(); + + return $this->request( + $this->context, + 'GET', + '/1/clusters/mapping/top', + null, + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout, + $requestHeaders + ); + } + + /** + * Search userIDs in the mapping + * return an object in the form: + * array( + * "hits" => array( + * array("userID" => "userName", "clusterName" => "name", "nbRecords" => 0, "dataSize" => 0) + * ), + * "nbHits" => 0 + * "page" => 0, + * "hitsPerPage" => 20 + * ). + * + * @return mixed + * @throws AlgoliaException + */ + public function searchUserIDs($query, $clusterName = null, $page = 0, $hitsPerPage = 20) + { + $requestHeaders = func_num_args() === 5 && is_array(func_get_arg(4)) ? func_get_arg(4) : array(); + + $params = array(); + + if ($query !== null) { + $params['query'] = $query; + } + + if ($clusterName !== null) { + $params['cluster'] = $clusterName; + } + + if ($page !== null) { + $params['page'] = $page; + } + + if ($hitsPerPage !== null) { + $params['hitsPerPage'] = $hitsPerPage; + } + + return $this->request( + $this->context, + 'POST', + '/1/clusters/mapping/search', + null, + $params, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->readTimeout, + $requestHeaders ); } @@ -414,6 +742,11 @@ public function initIndex($indexName) return new Index($this->context, $this, $indexName); } + public function initAnalytics() + { + return new Analytics($this); + } + /** * List all existing API keys with their associated ACLs. * @@ -423,6 +756,8 @@ public function initIndex($indexName) */ public function listApiKeys() { + $requestHeaders = func_num_args() === 1 && is_array(func_get_arg(0)) ? func_get_arg(0) : array(); + return $this->request( $this->context, 'GET', @@ -431,7 +766,8 @@ public function listApiKeys() null, $this->context->readHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -453,6 +789,8 @@ public function listUserKeys() */ public function getApiKey($key) { + $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); + return $this->request( $this->context, 'GET', @@ -461,7 +799,8 @@ public function getApiKey($key) null, $this->context->readHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -484,6 +823,8 @@ public function getUserKeyACL($key) */ public function deleteApiKey($key) { + $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); + return $this->request( $this->context, 'DELETE', @@ -492,7 +833,8 @@ public function deleteApiKey($key) null, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -542,11 +884,20 @@ public function deleteUserKey($key) */ public function addApiKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0, $indexes = null) { - if ($obj !== array_values($obj)) { // is dict of value + $requestHeaders = func_num_args() === 6 && is_array(func_get_arg(5)) ? func_get_arg(5) : array(); + + if ($obj !== array_values($obj)) { + // if $obj doesn't have required entries, we add the default values $params = $obj; - $params['validity'] = $validity; - $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour; - $params['maxHitsPerQuery'] = $maxHitsPerQuery; + if ($validity != 0) { + $params['validity'] = $validity; + } + if ($maxQueriesPerIPPerHour != 0) { + $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour; + } + if ($maxHitsPerQuery != 0) { + $params['maxHitsPerQuery'] = $maxHitsPerQuery; + } } else { $params = array( 'acl' => $obj, @@ -568,7 +919,8 @@ public function addApiKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $max $params, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -629,11 +981,20 @@ public function updateApiKey( $maxHitsPerQuery = 0, $indexes = null ) { - if ($obj !== array_values($obj)) { // is dict of value + $requestHeaders = func_num_args() === 7 && is_array(func_get_arg(6)) ? func_get_arg(6) : array(); + + if ($obj !== array_values($obj)) { + // if $obj doesn't have required entries, we add the default values $params = $obj; - $params['validity'] = $validity; - $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour; - $params['maxHitsPerQuery'] = $maxHitsPerQuery; + if ($validity != 0) { + $params['validity'] = $validity; + } + if ($maxQueriesPerIPPerHour != 0) { + $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour; + } + if ($maxHitsPerQuery != 0) { + $params['maxHitsPerQuery'] = $maxHitsPerQuery; + } } else { $params = array( 'acl' => $obj, @@ -642,6 +1003,7 @@ public function updateApiKey( 'maxHitsPerQuery' => $maxHitsPerQuery, ); } + if ($indexes != null) { $params['indexes'] = $indexes; } @@ -654,7 +1016,8 @@ public function updateApiKey( $params, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -676,27 +1039,33 @@ public function updateUserKey( $maxHitsPerQuery = 0, $indexes = null ) { - return $this->updateApiKey($key, $obj, $validity, $maxQueriesPerIPPerHour, $maxHitsPerQuery, $indexes); + $requestHeaders = func_num_args() === 7 && is_array(func_get_arg(6)) ? func_get_arg(6) : array(); + + return $this->updateApiKey($key, $obj, $validity, $maxQueriesPerIPPerHour, $maxHitsPerQuery, $indexes, $requestHeaders); } /** * Send a batch request targeting multiple indices. * - * @param array $requests an associative array defining the batch request body + * @param array $operations an associative array defining the batch request body + * @param array $requestHeaders * * @return mixed */ - public function batch($requests) + public function batch($operations) { + $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); + return $this->request( $this->context, 'POST', '/1/indexes/*/batch', array(), - array('requests' => $requests), + array('requests' => $operations), $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -781,6 +1150,7 @@ public static function buildQuery($args) * @param array $hostsArray * @param int $connectTimeout * @param int $readTimeout + * @param array $requestHeaders * * @return mixed * @@ -796,6 +1166,8 @@ public function request( $connectTimeout, $readTimeout ) { + $requestHeaders = func_num_args() === 9 && is_array(func_get_arg(8)) ? func_get_arg(8) : array(); + $exceptions = array(); $cnt = 0; foreach ($hostsArray as &$host) { @@ -805,7 +1177,7 @@ public function request( $readTimeout += 10; } try { - $res = $this->doRequest($context, $method, $host, $path, $params, $data, $connectTimeout, $readTimeout); + $res = $this->doRequest($context, $method, $host, $path, $params, $data, $connectTimeout, $readTimeout, $requestHeaders); if ($res !== null) { return $res; } @@ -831,6 +1203,7 @@ public function request( * @param array $data * @param int $connectTimeout * @param int $readTimeout + * @param array $requestHeaders * * @return mixed * @@ -847,6 +1220,8 @@ public function doRequest( $connectTimeout, $readTimeout ) { + $requestHeaders = func_num_args() === 9 && is_array(func_get_arg(8)) ? func_get_arg(8) : array(); + if (strpos($host, 'http') === 0) { $url = $host.$path; } else { @@ -897,7 +1272,7 @@ public function doRequest( ); } - $headers = array_merge($defaultHeaders, $context->headers); + $headers = array_merge($defaultHeaders, $context->headers, $requestHeaders); $curlHeaders = array(); foreach ($headers as $key => $value) { @@ -983,10 +1358,15 @@ public function doRequest( return; } - $answer = Json::decode($response, true); $context->releaseMHandle($curlHandle); curl_close($curlHandle); + if ($http_status == 204) { + return ''; + } + + $answer = Json::decode($response, true); + if (intval($http_status / 100) == 4) { throw new AlgoliaException(isset($answer['message']) ? $answer['message'] : $http_status.' error', $http_status); } elseif (intval($http_status / 100) != 2) { diff --git a/lib/AlgoliaSearch/ClientContext.php b/lib/AlgoliaSearch/ClientContext.php index 08c01ecf..2ad5ca59 100644 --- a/lib/AlgoliaSearch/ClientContext.php +++ b/lib/AlgoliaSearch/ClientContext.php @@ -109,7 +109,7 @@ public function __construct($applicationID = null, $apiKey = null, $hostsArray = if ($this->readHostsArray == null || count($this->readHostsArray) == 0) { $this->readHostsArray = $this->getDefaultReadHosts($placesEnabled); - $this->writeHostsArray = $this->getDefaultWriteHosts(); + $this->writeHostsArray = $this->getDefaultWriteHosts($placesEnabled); } if (($this->applicationID == null || mb_strlen($this->applicationID) == 0) && $placesEnabled === false) { @@ -167,10 +167,24 @@ private function getDefaultReadHosts($placesEnabled) } /** + * @param bool $placesEnabled + * * @return array */ - private function getDefaultWriteHosts() + private function getDefaultWriteHosts($placesEnabled) { + if ($placesEnabled) { + $hosts = array( + 'places-1.algolianet.com', + 'places-2.algolianet.com', + 'places-3.algolianet.com', + ); + shuffle($hosts); + array_unshift($hosts, 'places.algolia.net'); + + return $hosts; + } + $hosts = array( $this->applicationID.'-1.algolianet.com', $this->applicationID.'-2.algolianet.com', diff --git a/lib/AlgoliaSearch/Index.php b/lib/AlgoliaSearch/Index.php index 60ab28f6..aad7d7cc 100644 --- a/lib/AlgoliaSearch/Index.php +++ b/lib/AlgoliaSearch/Index.php @@ -27,6 +27,10 @@ namespace AlgoliaSearch; + +use AlgoliaSearch\Iterators\RuleIterator; +use AlgoliaSearch\Iterators\SynonymIterator; + /* * Contains all the functions related to one index * You should use Client.initIndex(indexName) to retrieve this object @@ -85,6 +89,8 @@ public function __construct(ClientContext $context, Client $client, $indexName) */ public function batchObjects($objects, $objectIDKey = 'objectID', $objectActionKey = 'objectAction') { + $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); + $requests = array(); $allowedActions = array( 'addObject', @@ -114,7 +120,7 @@ public function batchObjects($objects, $objectIDKey = 'objectID', $objectActionK $requests[] = $req; } - return $this->batch(array('requests' => $requests)); + return $this->batch(array('requests' => $requests), $requestHeaders); } /** @@ -129,6 +135,8 @@ public function batchObjects($objects, $objectIDKey = 'objectID', $objectActionK */ public function addObject($content, $objectID = null) { + $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); + if ($objectID === null) { return $this->client->request( $this->context, @@ -138,7 +146,8 @@ public function addObject($content, $objectID = null) $content, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -150,7 +159,8 @@ public function addObject($content, $objectID = null) $content, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -162,11 +172,23 @@ public function addObject($content, $objectID = null) * * @return mixed */ - public function addObjects($objects, $objectIDKey = 'objectID') + public function addObjects($objects, $objectIDKeyLegacy = 'objectID') { + + $objectIDKey = 'objectID'; + if (is_string($objectIDKeyLegacy)) { + $objectIDKey = $objectIDKeyLegacy; + } + + $requestHeaders = array(); + $nbArgs = func_num_args(); + if ($nbArgs > 1) { + $requestHeaders = is_array(func_get_arg($nbArgs-1)) ? func_get_arg($nbArgs-1) : array(); + } + $requests = $this->buildBatch('addObject', $objects, true, $objectIDKey); - return $this->batch($requests); + return $this->batch($requests, $requestHeaders); } /** @@ -174,11 +196,14 @@ public function addObjects($objects, $objectIDKey = 'objectID') * * @param string $objectID the unique identifier of the object to retrieve * @param string[] $attributesToRetrieve (optional) if set, contains the list of attributes to retrieve + * @param array $requestHeaders * * @return mixed */ public function getObject($objectID, $attributesToRetrieve = null) { + $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); + $id = urlencode($objectID); if ($attributesToRetrieve === null) { return $this->client->request( @@ -189,7 +214,8 @@ public function getObject($objectID, $attributesToRetrieve = null) null, $this->context->readHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -205,7 +231,8 @@ public function getObject($objectID, $attributesToRetrieve = null) null, $this->context->readHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -214,13 +241,16 @@ public function getObject($objectID, $attributesToRetrieve = null) * * @param array $objectIDs the array of unique identifier of objects to retrieve * @param string[] $attributesToRetrieve (optional) if set, contains the list of attributes to retrieve + * @param array $requestHeaders * * @return mixed * * @throws \Exception */ - public function getObjects($objectIDs, $attributesToRetrieve = null) + public function getObjects($objectIDs, $attributesToRetrieve = '') { + $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); + if ($objectIDs == null) { throw new \Exception('No list of objectID provided'); } @@ -248,7 +278,8 @@ public function getObjects($objectIDs, $attributesToRetrieve = null) array('requests' => $requests), $this->context->readHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -265,6 +296,8 @@ public function getObjects($objectIDs, $attributesToRetrieve = null) */ public function partialUpdateObject($partialObject, $createIfNotExists = true) { + $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); + $queryString = $createIfNotExists ? '' : '?createIfNotExists=false'; return $this->client->request( @@ -275,7 +308,8 @@ public function partialUpdateObject($partialObject, $createIfNotExists = true) $partialObject, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -283,33 +317,58 @@ public function partialUpdateObject($partialObject, $createIfNotExists = true) * Partially Override the content of several objects. * * @param array $objects contains an array of objects to update (each object must contains a objectID attribute) - * @param string $objectIDKey + * @param string $objectIDKey This parameter is deprecated and should not be used * @param bool $createIfNotExists * * @return mixed */ - public function partialUpdateObjects($objects, $objectIDKey = 'objectID', $createIfNotExists = true) + public function partialUpdateObjects($objects, $createIfNotExistsOrObjectIDKeyLegacy = 'objectID', $createIfNotExistsLegacy = true) { + if (is_bool($createIfNotExistsOrObjectIDKeyLegacy)) { + $createIfNotExists = $createIfNotExistsOrObjectIDKeyLegacy; + $objectIDKey = 'objectID'; + } elseif (is_string($createIfNotExistsOrObjectIDKeyLegacy)) { + $createIfNotExists = $createIfNotExistsLegacy; + $objectIDKey = $createIfNotExistsOrObjectIDKeyLegacy; + } + + $requestHeaders = array(); + $nbArgs = func_num_args(); + if ($nbArgs > 2) { + $requestHeaders = is_array(func_get_arg($nbArgs-1)) ? func_get_arg($nbArgs-1) : array(); + } + if ($createIfNotExists) { $requests = $this->buildBatch('partialUpdateObject', $objects, true, $objectIDKey); } else { $requests = $this->buildBatch('partialUpdateObjectNoCreate', $objects, true, $objectIDKey); } - return $this->batch($requests); + return $this->batch($requests, $requestHeaders); } /** * Override the content of object. * - * @param array $object contains the object to save, the object must contains an objectID attribute - * or attribute specified in $objectIDKey considered as objectID - * @param string $objectIDKey + * @param array $object contains the object to save, the object must contains an objectID attribute + * or attribute specified in $objectIDKey considered as objectID + * @param string $objectIDKey This parameter is deprecated and should not be used * * @return mixed */ - public function saveObject($object, $objectIDKey = 'objectID') + public function saveObject($object, $objectIDKeyLegacy = 'objectID') { + $objectIDKey = 'objectID'; + if (is_string($objectIDKeyLegacy)) { + $objectIDKey = $objectIDKeyLegacy; + } + + $requestHeaders = array(); + $nbArgs = func_num_args(); + if ($nbArgs > 1) { + $requestHeaders = is_array(func_get_arg($nbArgs-1)) ? func_get_arg($nbArgs-1) : array(); + } + return $this->client->request( $this->context, 'PUT', @@ -318,23 +377,35 @@ public function saveObject($object, $objectIDKey = 'objectID') $object, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } /** * Override the content of several objects. * - * @param array $objects contains an array of objects to update (each object must contains a objectID attribute) - * @param string $objectIDKey + * @param array $objects contains an array of objects to update (each object must contains a objectID attribute) + * @param string $objectIDKey This parameter is deprecated and should not be used * * @return mixed */ - public function saveObjects($objects, $objectIDKey = 'objectID') + public function saveObjects($objects, $objectIDKeyLegacy = 'objectID') { + $objectIDKey = 'objectID'; + if (is_string($objectIDKeyLegacy)) { + $objectIDKey = $objectIDKeyLegacy; + } + + $requestHeaders = array(); + $nbArgs = func_num_args(); + if ($nbArgs > 1) { + $requestHeaders = is_array(func_get_arg($nbArgs-1)) ? func_get_arg($nbArgs-1) : array(); + } + $requests = $this->buildBatch('updateObject', $objects, true, $objectIDKey); - return $this->batch($requests); + return $this->batch($requests, $requestHeaders); } /** @@ -349,6 +420,8 @@ public function saveObjects($objects, $objectIDKey = 'objectID') */ public function deleteObject($objectID) { + $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); + if ($objectID == null || mb_strlen($objectID) == 0) { throw new \Exception('objectID is mandatory'); } @@ -361,7 +434,8 @@ public function deleteObject($objectID) null, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -374,26 +448,31 @@ public function deleteObject($objectID) */ public function deleteObjects($objects) { + $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); + $objectIDs = array(); foreach ($objects as $key => $id) { $objectIDs[$key] = array('objectID' => $id); } $requests = $this->buildBatch('deleteObject', $objectIDs, true); - return $this->batch($requests); + return $this->batch($requests, $requestHeaders); } - public function deleteBy(array $args) + public function deleteBy(array $filterParameters) { + $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); + return $this->client->request( $this->context, 'POST', '/1/indexes/'.$this->urlIndexName.'/deleteByQuery', null, - array('params' => $this->client->buildQuery($args)), + array('params' => $this->client->buildQuery($filterParameters)), $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -413,24 +492,26 @@ public function deleteBy(array $args) */ public function deleteByQuery($query, $args = array(), $waitLastCall = true) { + $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); + $args['attributesToRetrieve'] = 'objectID'; $args['hitsPerPage'] = 1000; $args['distinct'] = false; $deletedCount = 0; - $results = $this->search($query, $args); + $results = $this->search($query, $args, $requestHeaders); while ($results['nbHits'] != 0) { $objectIDs = array(); foreach ($results['hits'] as $elt) { array_push($objectIDs, $elt['objectID']); } - $res = $this->deleteObjects($objectIDs); + $res = $this->deleteObjects($objectIDs, $requestHeaders); $deletedCount += count($objectIDs); if ($results['nbHits'] < $args['hitsPerPage'] && false === $waitLastCall) { break; } - $this->waitTask($res['taskID']); - $results = $this->search($query, $args); + $this->waitTask($res['taskID'], 100, $requestHeaders); + $results = $this->search($query, $args, $requestHeaders); } return $deletedCount; @@ -440,7 +521,7 @@ public function deleteByQuery($query, $args = array(), $waitLastCall = true) * Search inside the index. * * @param string $query the full text query - * @param mixed $args (optional) if set, contains an associative array with query parameters: + * @param mixed $searchParameters (optional) if set, contains an associative array with query parameters: * - page: (integer) Pagination parameter used to select the page to retrieve. * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 * - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. @@ -516,18 +597,22 @@ public function deleteByQuery($query, $args = array(), $waitLastCall = true) * duplicate value for the attributeForDistinct attribute are removed from results. For example, * if the chosen attribute is show_name and several hits have the same value for show_name, then * only the best one is kept and others are removed. + * @param array $requestHeaders + * * @return mixed * @throws AlgoliaException */ - public function search($query, $args = null) + public function search($query, $searchParameters = null) { - if ($args === null) { - $args = array(); + $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); + + if ($searchParameters === null) { + $searchParameters = array(); } - $args['query'] = $query; + $searchParameters['query'] = $query; - if (isset($args['disjunctiveFacets'])) { - return $this->searchWithDisjunctiveFaceting($query, $args); + if (isset($searchParameters['disjunctiveFacets'])) { + return $this->searchWithDisjunctiveFaceting($query, $searchParameters); } return $this->client->request( @@ -535,10 +620,11 @@ public function search($query, $args = null) 'POST', '/1/indexes/'.$this->urlIndexName.'/query', array(), - array('params' => $this->client->buildQuery($args)), + array('params' => $this->client->buildQuery($searchParameters)), $this->context->readHostsArray, $this->context->connectTimeout, - $this->context->searchTimeout + $this->context->searchTimeout, + $requestHeaders ); } @@ -550,6 +636,8 @@ public function search($query, $args = null) */ private function searchWithDisjunctiveFaceting($query, $args) { + $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); + if (! is_array($args['disjunctiveFacets']) || count($args['disjunctiveFacets']) <= 0) { throw new \InvalidArgumentException('disjunctiveFacets needs to be an non empty array'); } @@ -587,7 +675,12 @@ private function searchWithDisjunctiveFaceting($query, $args) /** * Do all queries in one call */ - $results = $this->client->multipleQueries(array_values($disjunctiveQueries)); + $results = $this->client->multipleQueries( + array_values($disjunctiveQueries), + 'indexName', + 'none', + $requestHeaders + ); $results = $results['results']; /** @@ -632,7 +725,8 @@ private function getDisjunctiveQueries($queryParams) 'page' => 0, 'attributesToRetrieve' => array(), 'attributesToHighlight' => array(), - 'attributesToSnippet' => array() + 'attributesToSnippet' => array(), + 'analytics' => false ); $additionalParams['facetFilters'] = $this->getAlgoliaFiltersArrayWithoutCurrentRefinement($facetFilters, $facetName . ':'); @@ -679,23 +773,27 @@ private function getAlgoliaFiltersArrayWithoutCurrentRefinement($filters, $needl * * @param $facetName * @param $facetQuery - * @param array $query + * @param array $searchParameters + * @param array $requestHeaders * * @return mixed */ - public function searchForFacetValues($facetName, $facetQuery, $query = array()) + public function searchForFacetValues($facetName, $facetQuery, $searchParameters = array()) { - $query['facetQuery'] = $facetQuery; + $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); + + $searchParameters['facetQuery'] = $facetQuery; return $this->client->request( $this->context, 'POST', '/1/indexes/'.$this->urlIndexName.'/facets/'.$facetName.'/query', array(), - array('params' => $this->client->buildQuery($query)), + array('params' => $this->client->buildQuery($searchParameters)), $this->context->readHostsArray, $this->context->connectTimeout, - $this->context->searchTimeout + $this->context->searchTimeout, + $requestHeaders ); } @@ -790,6 +888,10 @@ function ($val) use ($key) { $aggregated_answer = $answers['results'][0]; $aggregated_answer['disjunctiveFacets'] = array(); for ($i = 1; $i < count($answers['results']); $i++) { + if (!isset($answers['results'][$i]['facets'])) { + continue; + } + foreach ($answers['results'][$i]['facets'] as $key => $facet) { $aggregated_answer['disjunctiveFacets'][$key] = $facet; if (!in_array($key, $disjunctive_refinements)) { @@ -842,13 +944,9 @@ private function doBcBrowse($page = 0, $hitsPerPage = 1000) */ public function waitTask($taskID, $timeBeforeRetry = 100) { - while (true) { - $res = $this->getTaskStatus($taskID); - if ($res['status'] === 'published') { - return $res; - } - usleep($timeBeforeRetry * 1000); - } + $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); + + return $this->client->waitTask($this->indexName, $taskID, $timeBeforeRetry, $requestHeaders); } /** @@ -861,16 +959,9 @@ public function waitTask($taskID, $timeBeforeRetry = 100) */ public function getTaskStatus($taskID) { - return $this->client->request( - $this->context, - 'GET', - '/1/indexes/'.$this->urlIndexName.'/task/'.$taskID, - null, - null, - $this->context->readHostsArray, - $this->context->connectTimeout, - $this->context->readTimeout - ); + $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); + + return $this->client->getTaskStatus($this->indexName, $taskID, $requestHeaders); } /** @@ -882,6 +973,8 @@ public function getTaskStatus($taskID) */ public function getSettings() { + $requestHeaders = func_num_args() === 1 && is_array(func_get_arg(0)) ? func_get_arg(0) : array(); + return $this->client->request( $this->context, 'GET', @@ -890,7 +983,8 @@ public function getSettings() null, $this->context->readHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -903,6 +997,8 @@ public function getSettings() */ public function clearIndex() { + $requestHeaders = func_num_args() === 1 && is_array(func_get_arg(0)) ? func_get_arg(0) : array(); + return $this->client->request( $this->context, 'POST', @@ -911,7 +1007,8 @@ public function clearIndex() null, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -986,6 +1083,8 @@ public function clearIndex() */ public function setSettings($settings, $forwardToReplicas = false) { + $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); + $url = '/1/indexes/'.$this->urlIndexName.'/settings'; if ($forwardToReplicas) { @@ -1000,7 +1099,8 @@ public function setSettings($settings, $forwardToReplicas = false) $settings, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -1010,9 +1110,14 @@ public function setSettings($settings, $forwardToReplicas = false) * @return mixed * * @throws AlgoliaException + * + * @deprecated 1.26 All keys should be created with the Client class. + * If possible, delete keys attached to the index and re-create them with an index restriction */ public function listApiKeys() { + $requestHeaders = func_num_args() === 1 && is_array(func_get_arg(0)) ? func_get_arg(0) : array(); + return $this->client->request( $this->context, 'GET', @@ -1021,7 +1126,8 @@ public function listApiKeys() null, $this->context->readHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -1052,9 +1158,14 @@ public function getUserKeyACL($key) * @return mixed * * @throws AlgoliaException + * + * @deprecated 1.26 All keys should be created with the Client class. + * If possible, delete keys attached to the index and re-create them with an index restriction */ public function getApiKey($key) { + $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); + return $this->client->request( $this->context, 'GET', @@ -1063,7 +1174,8 @@ public function getApiKey($key) null, $this->context->readHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -1071,6 +1183,11 @@ public function getApiKey($key) /** * Delete an existing API key associated to this index. * + * All API keys should be created and modified using the Client class + * with an index restriction if necessary. + * Use this method to delete existing keys attached to the index but + * create new once with the client (to attach them to the app) + * * @param string $key * * @return mixed @@ -1079,6 +1196,8 @@ public function getApiKey($key) */ public function deleteApiKey($key) { + $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); + return $this->client->request( $this->context, 'DELETE', @@ -1087,7 +1206,8 @@ public function deleteApiKey($key) null, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -1133,15 +1253,24 @@ public function deleteUserKey($key) * @return mixed * * @throws AlgoliaException + * @deprecated 1.26 All API keys should be created and modified using the Client class with an index restriction if necessary. */ public function addApiKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) { - // is dict of value + $requestHeaders = func_num_args() === 5 && is_array(func_get_arg(4)) ? func_get_arg(4) : array(); + if ($obj !== array_values($obj)) { + // if $obj doesn't have required entries, we add the default values $params = $obj; - $params['validity'] = $validity; - $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour; - $params['maxHitsPerQuery'] = $maxHitsPerQuery; + if ($validity != 0) { + $params['validity'] = $validity; + } + if ($maxQueriesPerIPPerHour != 0) { + $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour; + } + if ($maxHitsPerQuery != 0) { + $params['maxHitsPerQuery'] = $maxHitsPerQuery; + } } else { $params = array( 'acl' => $obj, @@ -1159,7 +1288,8 @@ public function addApiKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $max $params, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -1169,7 +1299,7 @@ public function addApiKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $max * @param int $maxQueriesPerIPPerHour * @param int $maxHitsPerQuery * @return mixed - * @deprecated use addApiKey instead + * @deprecated 1.26 All API keys should be created and modified using the Client class with an index restriction if necessary. */ public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) { @@ -1210,15 +1340,24 @@ public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $ma * @return mixed * * @throws AlgoliaException + * @deprecated 1.26 All API keys should be created and modified using the Client class with an index restriction if necessary. */ public function updateApiKey($key, $obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) { - // is dict of value + $requestHeaders = func_num_args() === 6 && is_array(func_get_arg(5)) ? func_get_arg(5) : array(); + if ($obj !== array_values($obj)) { + // if $obj doesn't have required entries, we add the default values $params = $obj; - $params['validity'] = $validity; - $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour; - $params['maxHitsPerQuery'] = $maxHitsPerQuery; + if ($validity != 0) { + $params['validity'] = $validity; + } + if ($maxQueriesPerIPPerHour != 0) { + $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour; + } + if ($maxHitsPerQuery != 0) { + $params['maxHitsPerQuery'] = $maxHitsPerQuery; + } } else { $params = array( 'acl' => $obj, @@ -1236,7 +1375,8 @@ public function updateApiKey($key, $obj, $validity = 0, $maxQueriesPerIPPerHour $params, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -1257,21 +1397,25 @@ public function updateUserKey($key, $obj, $validity = 0, $maxQueriesPerIPPerHour /** * Send a batch request. * - * @param array $requests an associative array defining the batch request body + * @param array $operations an associative array defining the batch request body + * @param array $requestHeaders pass custom header only for this request * * @return mixed */ - public function batch($requests) + public function batch($operations) { + $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); + return $this->client->request( $this->context, 'POST', '/1/indexes/'.$this->urlIndexName.'/batch', array(), - $requests, + $operations, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -1305,20 +1449,23 @@ private function buildBatch($action, $objects, $withObjectID, $objectIDKey = 'ob * * @return IndexBrowser */ - private function doBrowse($query, $params = null) + private function doBrowse($query, $params = null, $requestHeaders = array()) { - return new IndexBrowser($this, $query, $params); + return new IndexBrowser($this, $query, $params, null, $requestHeaders); } /** * @param string $query * @param array|null $params * @param $cursor + * @param array $requestHeaders * * @return mixed */ public function browseFrom($query, $params = null, $cursor = null) { + $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); + if ($params === null) { $params = array(); } @@ -1327,22 +1474,24 @@ public function browseFrom($query, $params = null, $cursor = null) $params[$key] = Json::encode($value); } } - if ($query != null) { + if ($query !== null) { $params['query'] = $query; } - if ($cursor != null) { + + if ($cursor !== null) { $params['cursor'] = $cursor; } return $this->client->request( $this->context, - 'GET', + empty($params) ? 'GET' : 'POST', '/1/indexes/'.$this->urlIndexName.'/browse', - $params, null, + $params, $this->context->readHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -1356,8 +1505,10 @@ public function browseFrom($query, $params = null, $cursor = null) * * @throws AlgoliaException */ - public function searchSynonyms($query, array $synonymType = array(), $page = null, $hitsPerPage = null) + public function searchSynonyms($query, array $synonymType = array(), $page = 0, $hitsPerPage = 100) { + $requestHeaders = func_num_args() === 5 && is_array(func_get_arg(4)) ? func_get_arg(4) : array(); + $params = array(); if ($query !== null) { @@ -1393,7 +1544,8 @@ public function searchSynonyms($query, array $synonymType = array(), $page = nul $params, $this->context->readHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -1406,6 +1558,8 @@ public function searchSynonyms($query, array $synonymType = array(), $page = nul */ public function getSynonym($objectID) { + $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); + return $this->client->request( $this->context, 'GET', @@ -1414,7 +1568,8 @@ public function getSynonym($objectID) null, $this->context->readHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -1428,6 +1583,8 @@ public function getSynonym($objectID) */ public function deleteSynonym($objectID, $forwardToReplicas = false) { + $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); + return $this->client->request( $this->context, 'DELETE', @@ -1436,7 +1593,8 @@ public function deleteSynonym($objectID, $forwardToReplicas = false) null, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -1449,6 +1607,8 @@ public function deleteSynonym($objectID, $forwardToReplicas = false) */ public function clearSynonyms($forwardToReplicas = false) { + $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); + return $this->client->request( $this->context, 'POST', @@ -1457,7 +1617,8 @@ public function clearSynonyms($forwardToReplicas = false) null, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -1472,6 +1633,8 @@ public function clearSynonyms($forwardToReplicas = false) */ public function batchSynonyms($objects, $forwardToReplicas = false, $replaceExistingSynonyms = false) { + $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); + return $this->client->request( $this->context, 'POST', @@ -1481,7 +1644,8 @@ public function batchSynonyms($objects, $forwardToReplicas = false, $replaceExis $objects, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } @@ -1496,6 +1660,8 @@ public function batchSynonyms($objects, $forwardToReplicas = false, $replaceExis */ public function saveSynonym($objectID, $content, $forwardToReplicas = false) { + $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); + return $this->client->request( $this->context, 'PUT', @@ -1504,10 +1670,21 @@ public function saveSynonym($objectID, $content, $forwardToReplicas = false) $content, $this->context->writeHostsArray, $this->context->connectTimeout, - $this->context->readTimeout + $this->context->readTimeout, + $requestHeaders ); } + /** + * @param int $batchSize + * + * @return SynonymIterator + */ + public function initSynonymIterator($batchSize = 1000) + { + return new SynonymIterator($this, $batchSize); + } + /** * @deprecated Please use searchForFacetValues instead * @param $facetName @@ -1644,6 +1821,10 @@ public function saveRule($objectID, $content, $forwardToReplicas = false) $content['objectID'] = $objectID; } + if (! $content['objectID']) { + throw new AlgoliaException('Cannot save the rule because `objectID` must be set and non-empty.'); + } + return $this->client->request( $this->context, 'PUT', @@ -1656,6 +1837,16 @@ public function saveRule($objectID, $content, $forwardToReplicas = false) ); } + /** + * @param int $batchSize + * + * @return RuleIterator + */ + public function initRuleIterator($batchSize = 500) + { + return new RuleIterator($this, $batchSize); + } + /** * @param string $name * @param array $arguments diff --git a/lib/AlgoliaSearch/IndexBrowser.php b/lib/AlgoliaSearch/IndexBrowser.php index 9ebb5e45..eef0c38b 100644 --- a/lib/AlgoliaSearch/IndexBrowser.php +++ b/lib/AlgoliaSearch/IndexBrowser.php @@ -71,8 +71,9 @@ class IndexBrowser implements \Iterator * @param string $query * @param array|null $params * @param int|null $cursor + * @param array $requestHeaders */ - public function __construct(Index $index, $query, $params = null, $cursor = null) + public function __construct(Index $index, $query, $params = null, $cursor = null, $requestHeaders = array()) { $this->index = $index; $this->query = $query; @@ -80,7 +81,7 @@ public function __construct(Index $index, $query, $params = null, $cursor = null $this->position = 0; - $this->doQuery($cursor); + $this->doQuery($cursor, $requestHeaders); } /** @@ -148,13 +149,14 @@ public function cursor() /** * @param int $cursor + * @param array $requestHeaders */ - private function doQuery($cursor = null) + private function doQuery($cursor = null, $requestHeaders = array()) { if ($cursor !== null) { $this->params['cursor'] = $cursor; } - $this->answer = $this->index->browseFrom($this->query, $this->params, $cursor); + $this->answer = $this->index->browseFrom($this->query, $this->params, $cursor, $requestHeaders); } } diff --git a/lib/AlgoliaSearch/Iterators/AlgoliaIterator.php b/lib/AlgoliaSearch/Iterators/AlgoliaIterator.php new file mode 100644 index 00000000..53dab585 --- /dev/null +++ b/lib/AlgoliaSearch/Iterators/AlgoliaIterator.php @@ -0,0 +1,152 @@ +index = $index; + $this->hitsPerPage = (int) $hitsPerPage; + } + + /** + * Return the current element + * @return array + */ + public function current() + { + $this->ensureResponseExists(); + $hit = $this->response['hits'][$this->getHitIndexForCurrentPage()]; + + return $this->formatHit($hit); + } + + /** + * Move forward to next element + * @return void Any returned value is ignored. + */ + public function next() + { + $previousPage = $this->getCurrentPage(); + $this->key++; + if($this->getCurrentPage() !== $previousPage) { + // Discard the response if the page has changed. + $this->response = null; + } + } + + /** + * Return the key of the current element + * @return int + */ + public function key() + { + return $this->key; + } + + /** + * Checks if current position is valid. If the current position + * is not valid, we call Algolia' API to load more results + * until it's the last page. + * + * @return boolean The return value will be casted to boolean and then evaluated. + * Returns true on success or false on failure. + */ + public function valid() + { + $this->ensureResponseExists(); + + return isset($this->response['hits'][$this->getHitIndexForCurrentPage()]); + } + + /** + * Rewind the Iterator to the first element + * @return void Any returned value is ignored. + */ + public function rewind() + { + $this->key = 0; + $this->response = null; + } + + /** + * ensureResponseExists is always called prior + * to trying to access the response property. + */ + protected function ensureResponseExists() { + if ($this->response === null) { + $this->fetchCurrentPageResults(); + } + } + + /** + * getCurrentPage returns the current zero based page according to + * the current key and hits per page. + * + * @return int + */ + protected function getCurrentPage() + { + return (int) floor($this->key / ($this->hitsPerPage)); + } + + /** + * getHitIndexForCurrentPage retrieves the index + * of the hit in the current page. + * + * @return int + */ + protected function getHitIndexForCurrentPage() + { + return $this->key - ($this->getCurrentPage() * $this->hitsPerPage); + } + + /** + * Call Algolia' API to get new result batch + */ + abstract protected function fetchCurrentPageResults(); + + /** + * The export method might be is using search internally, this method + * is used to clean the results, like remove the highlight + * + * @param array $hit + * @return array formatted synonym array + */ + abstract protected function formatHit(array $hit); +} diff --git a/lib/AlgoliaSearch/Iterators/RuleIterator.php b/lib/AlgoliaSearch/Iterators/RuleIterator.php new file mode 100644 index 00000000..11b41834 --- /dev/null +++ b/lib/AlgoliaSearch/Iterators/RuleIterator.php @@ -0,0 +1,39 @@ +response = $this->index->searchRules(array( + 'hitsPerPage' => $this->hitsPerPage, + 'page' => $this->getCurrentPage(), + )); + } +} diff --git a/lib/AlgoliaSearch/Iterators/SynonymIterator.php b/lib/AlgoliaSearch/Iterators/SynonymIterator.php new file mode 100644 index 00000000..fd5d2b1d --- /dev/null +++ b/lib/AlgoliaSearch/Iterators/SynonymIterator.php @@ -0,0 +1,29 @@ +response = $this->index->searchSynonyms('', array(), $this->getCurrentPage(), $this->hitsPerPage); + } +} diff --git a/lib/AlgoliaSearch/PlacesIndex.php b/lib/AlgoliaSearch/PlacesIndex.php index 17911b8d..55b5713c 100644 --- a/lib/AlgoliaSearch/PlacesIndex.php +++ b/lib/AlgoliaSearch/PlacesIndex.php @@ -76,6 +76,27 @@ public function search($query, $args = null) ); } + /** + * @param mixed $objectID + * + * @return mixed + * + * @throws AlgoliaException + */ + public function getObject($objectID) + { + return $this->client->request( + $this->context, + 'GET', + '/1/places/' . urlencode($objectID), + null, + null, + $this->context->readHostsArray, + $this->context->connectTimeout, + $this->context->searchTimeout + ); + } + /** * @param string $key * @param string $value diff --git a/lib/AlgoliaSearch/Version.php b/lib/AlgoliaSearch/Version.php index 0eb09f02..e2c67fe9 100644 --- a/lib/AlgoliaSearch/Version.php +++ b/lib/AlgoliaSearch/Version.php @@ -29,11 +29,11 @@ class Version { - const VALUE = '1.19.0'; + const VALUE = '1.28.0'; public static $custom_value = ''; - private static $prefixUserAgentSegments = ''; + private static $defaultUserAgentSegments = ''; private static $suffixUserAgentSegments = ''; // Method untouched to keep backward compatibility @@ -44,7 +44,21 @@ public static function get() public static function getUserAgent() { - $userAgent = self::$prefixUserAgentSegments.'Algolia for PHP ('.self::VALUE.')'.static::$suffixUserAgentSegments; + if (!self::$defaultUserAgentSegments) { + $version = PHP_VERSION; + if ($hyphen = strpos($version, '-')) { + $version = substr($version, 0, $hyphen); + } + self::$defaultUserAgentSegments = + 'Algolia for PHP ('.self::VALUE.'); ' . + 'PHP ('.$version.')'; + + if (defined('HHVM_VERSION')) { + self::$defaultUserAgentSegments .= '; HHVM ('.HHVM_VERSION.')'; + } + } + + $userAgent = self::$defaultUserAgentSegments . static::$suffixUserAgentSegments; // Keep backward compatibility $userAgent .= static::$custom_value; @@ -54,11 +68,7 @@ public static function getUserAgent() public static function addPrefixUserAgentSegment($segment, $version) { - $prefix = $segment.' ('.$version.'); '; - - if (false === mb_strpos(self::getUserAgent(), $prefix)) { - self::$prefixUserAgentSegments = $prefix . self::$prefixUserAgentSegments; - } + self::addSuffixUserAgentSegment($segment, $version); } public static function addSuffixUserAgentSegment($segment, $version) @@ -73,6 +83,6 @@ public static function addSuffixUserAgentSegment($segment, $version) public static function clearUserAgentSuffixesAndPrefixes() { self::$suffixUserAgentSegments = ''; - self::$prefixUserAgentSegments = ''; + self::$defaultUserAgentSegments = ''; } } diff --git a/lib/AlgoliaSearch/loader.php b/lib/AlgoliaSearch/loader.php index 7f26f13b..654becaf 100644 --- a/lib/AlgoliaSearch/loader.php +++ b/lib/AlgoliaSearch/loader.php @@ -26,6 +26,7 @@ require_once __DIR__.'/AlgoliaException.php'; require_once __DIR__.'/AlgoliaConnectionException.php'; +require_once __DIR__.'/Analytics.php'; require_once __DIR__.'/Client.php'; require_once __DIR__.'/ClientContext.php'; require_once __DIR__.'/FailingHostsCache.php'; @@ -37,3 +38,6 @@ require_once __DIR__.'/PlacesIndex.php'; require_once __DIR__.'/SynonymType.php'; require_once __DIR__.'/Version.php'; +require_once __DIR__.'/Iterators/AlgoliaIterator.php'; +require_once __DIR__.'/Iterators/RuleIterator.php'; +require_once __DIR__.'/Iterators/SynonymIterator.php';