├── algolia.php ├── lib ├── algolia.php └── algolia │ └── results.php ├── package.json ├── readme.md ├── vendor └── algolia-client │ ├── LICENSE.txt │ ├── algoliasearch.php │ ├── resources │ └── ca-bundle.crt │ └── src │ └── AlgoliaSearch │ ├── AlgoliaConnectionException.php │ ├── AlgoliaException.php │ ├── Client.php │ ├── ClientContext.php │ ├── FailingHostsCache.php │ ├── FileFailingHostsCache.php │ ├── InMemoryFailingHostsCache.php │ ├── Index.php │ ├── IndexBrowser.php │ ├── Iterators │ ├── AlgoliaIterator.php │ ├── RuleIterator.php │ └── SynonymIterator.php │ ├── Json.php │ ├── PlacesIndex.php │ ├── SynonymType.php │ └── Version.php └── widgets └── algolia ├── algolia.html.php └── algolia.php /algolia.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | /** 10 | * Autoloader for all Kirby Algolia classes and dependencies 11 | */ 12 | load([ 13 | 'kirby\\algolia' => __DIR__ . DS . 'lib' . DS . 'algolia.php', 14 | 'kirby\\algolia\\results' => __DIR__ . DS . 'lib' . DS . 'algolia' . DS . 'results.php', 15 | 16 | // Official Algolia PHP API client 17 | 'algoliasearch\\client' => __DIR__ . DS . 'vendor' . DS . 'algolia-client' . DS . 'algoliasearch.php' 18 | ]); 19 | 20 | /** 21 | * Helper function that returns a Kirby\Algolia instance 22 | * 23 | * @return Algolia 24 | */ 25 | function algolia() { 26 | return Kirby\Algolia::instance(); 27 | } 28 | 29 | // Register the Panel widget 30 | if(c::get('algolia.widget', true) && function_exists('panel')) { 31 | kirby()->set('widget', 'algolia', __DIR__ . DS . 'widgets' . DS . 'algolia'); 32 | 33 | // Register the route for the widget 34 | panel()->routes([ 35 | [ 36 | 'pattern' => 'widgets/algolia/index', 37 | 'method' => 'GET', 38 | 'filter' => 'auth', 39 | 'action' => function() { 40 | algolia()->index(); 41 | 42 | panel()->notify(':)'); 43 | panel()->redirect('/'); 44 | } 45 | ] 46 | ]); 47 | } 48 | 49 | /** 50 | * Panel hooks 51 | * 52 | * Every page change is automatically synced to Algolia 53 | * Automatic indexing can be disabled with the algolia.autoindex option 54 | */ 55 | 56 | if(c::get('algolia.autoindex', true)) { 57 | kirby()->hook('panel.page.create', function($page) { 58 | return algolia()->insertPage($page); 59 | }); 60 | 61 | kirby()->hook('panel.page.update', function($page) { 62 | return algolia()->updatePage($page); 63 | }); 64 | 65 | kirby()->hook('panel.page.delete', function($page) { 66 | return algolia()->deletePageRecursive($page); 67 | }); 68 | 69 | kirby()->hook('panel.page.sort', function($page) { 70 | return algolia()->updatePage($page); 71 | }); 72 | 73 | kirby()->hook('panel.page.hide', function($page) { 74 | return algolia()->updatePage($page); 75 | }); 76 | 77 | kirby()->hook('panel.page.move', function($newPage, $oldPage) { 78 | return algolia()->movePage($oldPage, $newPage); 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /lib/algolia.php: -------------------------------------------------------------------------------- 1 | 22 | * @license MIT 23 | * @link https://getkirby.com 24 | */ 25 | class Algolia { 26 | // Singleton class instance 27 | public static $instance; 28 | 29 | // Algolia client instance 30 | protected $algolia; 31 | 32 | // Caches 33 | protected $indexCache = array(); 34 | 35 | /** 36 | * Class constructor 37 | */ 38 | public function __construct() { 39 | $app = c::get('algolia.app'); 40 | $key = c::get('algolia.key'); 41 | 42 | if(!$app || !$key) throw new Error('Please set your Algolia API credentials in the Kirby configuration.'); 43 | 44 | $this->algolia = new AlgoliaClient($app, $key); 45 | } 46 | 47 | /** 48 | * Returns a singleton instance of the Algolia class 49 | * 50 | * @return Algolia 51 | */ 52 | public static function instance() { 53 | if(static::$instance) return static::$instance; 54 | return static::$instance = new static; 55 | } 56 | 57 | /** 58 | * Sends a search query to Algolia and returns a paginated collection of results 59 | * 60 | * @param string $query Search query 61 | * @param integer $page Pagination page to return (starts at 1, not 0!) 62 | * @param array $options Search parameters to override the default settings 63 | * See https://www.algolia.com/doc/php#full-text-search-parameters 64 | * @return Collection 65 | */ 66 | public function search($query, $page = 1, $options = array()) { 67 | $defaultOptions = c::get('algolia.search.options', array()); 68 | $options = array_merge($defaultOptions, $options); 69 | 70 | // Set the page parameter 71 | // Algolia uses zero based page indexes while Kirby's pagination starts at 1 72 | $options['page'] = ($page)? $page - 1 : 0; 73 | 74 | // Start the search 75 | $results = $this->getIndex()->search($query, $options); 76 | 77 | // Return a collection of the results 78 | return new Results($results); 79 | } 80 | 81 | /** 82 | * Indexes everything and replaces the current index 83 | * 84 | * Uses atomical re-indexing: 85 | * https://www.algolia.com/doc/faq/index-configuration/how-can-i-update-all-the-objects-of-my-index 86 | */ 87 | public function index() { 88 | // Get the settings from the main index 89 | $mainIndex = $this->getIndex(); 90 | $settings = $mainIndex->getSettings(); 91 | 92 | // Make sure that we don't copy over slaves 93 | $settings['slaves'] = array(); 94 | 95 | // Save the settings into a fresh temp index 96 | $tempIndex = $this->getIndex(true); 97 | $tempIndex->setSettings($settings); 98 | $tempIndex->clearIndex(); 99 | 100 | // Add all objects back in 101 | $queue = array(); 102 | foreach(site()->index()->filter(array($this, 'isIndexable')) as $p) { 103 | $queue[] = $this->formatPage($p); 104 | 105 | // Always upload objects in batches of 100 for performance reasons 106 | if(count($queue) >= 100) { 107 | $tempIndex->saveObjects($queue); 108 | $queue = array(); 109 | } 110 | } 111 | 112 | // Upload the remaining objects 113 | $tempIndex->saveObjects($queue); 114 | 115 | // Move the temp index to the main index 116 | $this->algolia->moveIndex($this->getIndexName(true), $this->getIndexName()); 117 | } 118 | 119 | /** 120 | * Inserts a page into the index 121 | * Used by Panel hooks 122 | * 123 | * @param Page $page Kirby page 124 | */ 125 | public function insertPage(Page $page) { 126 | if(!static::isIndexable($page)) return; 127 | 128 | $this->getIndex()->saveObject(static::formatPage($page)); 129 | } 130 | 131 | /** 132 | * Updates a page in the index 133 | * Used by Panel hooks 134 | * 135 | * @param Page $page Kirby page 136 | */ 137 | public function updatePage(Page $page) { 138 | if(!static::isIndexable($page)) { 139 | // Delete the page from the index 140 | $this->deletePage($page); 141 | return; 142 | } 143 | 144 | $this->getIndex()->saveObject(static::formatPage($page)); 145 | } 146 | 147 | /** 148 | * Moves a page in the index 149 | * Used by Panel hooks 150 | * 151 | * @param Page $oldPage Kirby page object before the move 152 | * @param Page $newPage Kirby page object after the move 153 | */ 154 | public function movePage(Page $oldPage, Page $newPage) { 155 | // Delete the old object 156 | $this->deletePage($oldPage); 157 | 158 | // Insert the new object 159 | $this->insertPage($newPage); 160 | } 161 | 162 | /** 163 | * Deletes a page from the index 164 | * 165 | * @param Page|string $id Kirby page or page ID 166 | */ 167 | public function deletePage($id) { 168 | if($id instanceof Page) $id = $id->id(); 169 | 170 | $this->getIndex()->deleteObject($id); 171 | } 172 | 173 | /** 174 | * Deletes a page and all its children from the index 175 | * Used by Panel hooks 176 | * 177 | * @param Page|string $page Kirby page or page ID 178 | */ 179 | public function deletePageRecursive($page) { 180 | if(is_string($page)) $page = page($page); 181 | if(!$page) return false; 182 | 183 | $this->deletePage($page); 184 | foreach($page->children() as $p) { 185 | $this->deletePageRecursive($p); 186 | } 187 | } 188 | 189 | /** 190 | * Checks if a specific page should be included in the Algolia index 191 | * Uses the configuration option algolia.templates 192 | * 193 | * @param Page $page Kirby page 194 | * @return boolean 195 | */ 196 | public static function isIndexable(Page $page) { 197 | $templates = c::get('algolia.templates', array()); 198 | $pageTemplate = $page->intendedTemplate(); 199 | 200 | // Quickly whitelist simple definitions 201 | // Example: array('project') 202 | if(in_array($pageTemplate, $templates, true)) return true; 203 | 204 | // Sort out pages whose template is not defined 205 | if(!isset($templates[$pageTemplate])) return false; 206 | $template = $templates[$pageTemplate]; 207 | 208 | // Check if the template is defined as a boolean 209 | // Example: array('project' => true, 'contact' => false) 210 | if(is_bool($template)) return $template; 211 | 212 | // Skip every value that is not a boolean or array for consistency 213 | if(!is_array($template)) return false; 214 | 215 | // Check for the custom filter function 216 | // Example: array('project' => array('filter' => function($page) {...})) 217 | if(isset($template['filter'])) { 218 | $filter = $template['filter']; 219 | if(is_callable($filter) && !call_user_func($filter, $page)) return false; 220 | } 221 | 222 | // No rule was violated, the page is indexable 223 | return true; 224 | } 225 | 226 | /** 227 | * Converts a page into a data array for Algolia 228 | * Uses the configuration options algolia.fields and algolia.templates 229 | * 230 | * @param Page $page Kirby page 231 | * @return array 232 | */ 233 | public static function formatPage(Page $page) { 234 | $fields = c::get('algolia.fields', array('url', 'intendedTemplate')); 235 | 236 | $templates = c::get('algolia.templates', array()); 237 | $pageTemplate = $page->intendedTemplate(); 238 | 239 | // Merge fields with the default fields and make array structure consistent 240 | if(isset($templates[$pageTemplate]['fields'])) { 241 | $fields = array_merge( 242 | static::cleanUpFields($fields), 243 | static::cleanUpFields($templates[$pageTemplate]['fields']) 244 | ); 245 | } else { 246 | $fields = static::cleanUpFields($fields); 247 | } 248 | 249 | // Build resulting data array 250 | $data = array('objectID' => $page->id()); 251 | foreach($fields as $name => $operation) { 252 | if(is_callable($operation)) { 253 | // Custom function 254 | $data[$name] = call_user_func($operation, $page); 255 | } else if(is_string($operation)) { 256 | // Field method without parameters 257 | $result = $page->$name(); 258 | if(!($result instanceof Field)) { 259 | $result = new Field($page, $name, $result); 260 | } 261 | 262 | $result = $result->$operation(); 263 | 264 | // Make sure that the result is not an object 265 | $data[$name] = (is_object($result))? (string)$result : $result; 266 | } else if(is_array($operation)) { 267 | // Field method with parameters 268 | $result = $page->$name(); 269 | 270 | // Skip invalid definitions 271 | if(!isset($operation[0])) { 272 | $data[$name] = (string)$result; 273 | continue; 274 | } 275 | 276 | if(!($result instanceof Field)) { 277 | $result = new Field($page, $name, $result); 278 | } 279 | 280 | $parameters = array_slice($operation, 1); 281 | $operation = $operation[0]; 282 | $result = call_user_func_array(array($result, $operation), $parameters); 283 | 284 | // Make sure that the result is not an object 285 | $data[$name] = (is_object($result))? (string)$result : $result; 286 | } else { 287 | // No or invalid operation, convert to string 288 | $data[$name] = (string)$page->$name(); 289 | } 290 | } 291 | 292 | return $data; 293 | } 294 | 295 | /** 296 | * Returns the number of indexable pages 297 | * 298 | * @return int 299 | */ 300 | public function objectCount() { 301 | return site()->index()->filter(array($this, 'isIndexable'))->count(); 302 | } 303 | 304 | /** 305 | * Returns the correct Algolia index (temporary or main) 306 | * 307 | * @param boolean $temp If true, returns the temporary index 308 | * @return AlgoliaSearch\Index 309 | */ 310 | protected function getIndex($temp = false) { 311 | $index = $this->getIndexName($temp); 312 | 313 | if(isset($this->indexCache[$index])) return $this->indexCache[$index]; 314 | return $this->indexCache[$index] = $this->algolia->initIndex($index); 315 | } 316 | 317 | /** 318 | * Returns the name of the correct Algolia index (temporary or main) 319 | * 320 | * @param boolean $temp If true, returns the name of the temporary index 321 | * @return string 322 | */ 323 | protected function getIndexName($temp = false) { 324 | $index = c::get('algolia.index', 'kirby'); 325 | 326 | if($temp) { 327 | return c::get('algolia.index.temp', $index . '_temp'); 328 | } else { 329 | return $index; 330 | } 331 | } 332 | 333 | /** 334 | * Makes an array of fields and operations consistent 335 | * for formatPage() 336 | * 337 | * @param array $fields 338 | * @return array 339 | */ 340 | protected static function cleanUpFields($fields) { 341 | $result = array(); 342 | 343 | foreach($fields as $name => $operation) { 344 | // Make sure the name is always the key, even if no operation was given 345 | if(is_int($name)) { 346 | $name = $operation; 347 | $operation = null; 348 | } 349 | 350 | $result[$name] = $operation; 351 | } 352 | 353 | // Make sure that the fields are sorted alphabetically for consistence 354 | ksort($result); 355 | return $result; 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /lib/algolia/results.php: -------------------------------------------------------------------------------- 1 | 14 | * @license MIT 15 | * @link https://getkirby.com 16 | */ 17 | class Results extends Collection { 18 | // Result metadata 19 | protected $totalCount; 20 | protected $processingTime; 21 | protected $query; 22 | protected $params; 23 | 24 | /** 25 | * Class constructor 26 | * 27 | * @param array $results Returned data from an Algolia search operation 28 | */ 29 | public function __construct($results) { 30 | // Defaults in case the results are invalid 31 | $defaults = array( 32 | 'hits' => array(), 33 | 'page' => 0, 34 | 'nbHits' => 0, 35 | 'nbPages' => 0, 36 | 'hitsPerPage' => 20, 37 | 'processingTimeMS' => 0, 38 | 'query' => '', 39 | 'params' => '' 40 | ); 41 | $results = array_merge($defaults, $results); 42 | 43 | // Convert the hits to Obj objects 44 | $hits = array_map(function($hit) { 45 | return new Obj($hit); 46 | }, $results['hits']); 47 | 48 | // Get metadata from the results 49 | // Algolia uses zero based page indexes while Kirby's pagination starts at 1 50 | $page = $results['page'] + 1; 51 | $totalCount = $results['nbHits']; 52 | $hitsPerPage = $results['hitsPerPage']; 53 | $processingTime = $results['processingTimeMS']; 54 | $query = $results['query']; 55 | $params = $results['params']; 56 | 57 | // Store the results 58 | parent::__construct($hits); 59 | $this->totalCount = $totalCount; 60 | $this->processingTime = $processingTime; 61 | $this->query = $query; 62 | $this->params = $params; 63 | 64 | // Paginate the collection 65 | $pagination = new Pagination($totalCount, $hitsPerPage, compact('page')); 66 | $this->paginate($pagination); 67 | } 68 | 69 | /** 70 | * Returns the total count of results for the search query 71 | * $results->count() returns the count of results on the current pagination page 72 | * 73 | * @return int 74 | */ 75 | public function totalCount() { 76 | return $this->totalCount; 77 | } 78 | 79 | /** 80 | * Returns the Algolia server processing time in ms 81 | * 82 | * @return int 83 | */ 84 | public function processingTime() { 85 | return $this->processingTime; 86 | } 87 | 88 | /** 89 | * Returns the search query 90 | * 91 | * @return string 92 | */ 93 | public function query() { 94 | return $this->query; 95 | } 96 | 97 | /** 98 | * Returns the Algolia search parameter string 99 | * Useful when debugging search requests 100 | * 101 | * @return string 102 | */ 103 | public function params() { 104 | return $this->params; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "algolia", 3 | "description": "Kirby Algolia Plugin", 4 | "author": "Lukas Bestle ", 5 | "license": "MIT", 6 | "version": "1.1.3", 7 | "type": "kirby-plugin" 8 | } 9 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Kirby 2 Algolia Plugin 2 | 3 | This plugin integrates with the [Algolia service](https://www.algolia.com) to index and search through your Kirby website. 4 | 5 | ## What is Algolia? 6 | 7 | Algolia is a hosted search engine for websites and apps. After providing it with the structured content of your website, it will create an optimized database of the content that will be very fast to search through via their API. 8 | 9 | Using Algolia requires an [account](https://www.algolia.com/users/sign_up). Free accounts are available, but paid accounts allow you to use the service without displaying the Algolia logo next to the search results. The [pricing](https://www.algolia.com/pricing) mostly depends on the number of records (Kirby pages) you want to index, the change frequency of these records and the number of search queries per month. The "Starter" plan should be enough for most Kirby websites. 10 | 11 | You can read more about Algolia on their [website](https://www.algolia.com) and in their [documentation](https://www.algolia.com/doc). 12 | 13 | ## Requirements 14 | 15 | This plugin requires Kirby 2.3 or later and an account at Algolia. 16 | 17 | ## Installation 18 | 19 | To install the plugin, please put it in the `site/plugins` directory and add the necessary configuration options (see below) to your `site/config/config.php`. 20 | 21 | ## Configuration 22 | 23 | ### API credentials 24 | 25 | After [creating an Algolia account](https://www.algolia.com/users/sign_up), you can find your API credentials in the Algolia dashboard on the "API keys" page. The values you need are the "Application ID" and the "Write API Key": 26 | 27 | ```php 28 | c::set('algolia.app', 'ABCDEFGHIJ'); 29 | c::set('algolia.key', '12345678901234567890123456789012'); 30 | ``` 31 | 32 | ### Search index 33 | 34 | Once setup, the plugin will upload your Kirby pages to an Algolia index. An index is like a database table that is optimized for searching. 35 | You need to create one index per site in the [Algolia dashboard](https://www.algolia.com/dashboard) and set its name like this: 36 | 37 | ```php 38 | c::set('algolia.index', 'myindex'); 39 | ``` 40 | 41 | The plugin also uses a temporary index when re-indexing the site manually. This temporary index is created automatically when needed. 42 | The name of that index is set to `{{algolia.index}}_temp` by default, but if you need to, you can change this value like this: 43 | 44 | ```php 45 | c::set('algolia.index.temp', 'myindex_temp'); 46 | ``` 47 | 48 | ### Indexing options 49 | 50 | #### Template specific settings 51 | 52 | This is the most important indexing option that allows you to define which pages and which fields of the pages should be uploaded and indexed. 53 | You can define these rules for each template. Every template that is not in the list will be ignored: 54 | 55 | ```php 56 | c::set('algolia.templates', array( 57 | // Simple definition if you don't need a filter and don't want to add fields (see below) 58 | 'contact', 59 | 60 | // Complex definition that allows to customize the behavior 61 | 'project' => array( 62 | // A filter function can decide whether a specific page should be indexed or not 63 | 'filter' => function($page) { 64 | return $page->isVisible() && $page->indexable()->bool(); 65 | }, 66 | 67 | // List of fields to send to Algolia 68 | // Extends the algolia.fields option (see below) 69 | 'fields' => array( 70 | // Simple string values 71 | 'title', 72 | 'text', 73 | 74 | // Define a field method to transform the field 75 | 'featured' => 'bool', 76 | 77 | // Field method with arguments 78 | 'tags' => array('split', ','), 79 | 80 | // Custom value using a function 81 | 'image' => function($page) { 82 | // Returns the URL of the first image of the page 83 | // You should always make sure to return something here, even if there are no images, to avoid errors 84 | $image = $page->images()->first(); 85 | return ($image)? $image->url() : null; 86 | } 87 | ) 88 | ) 89 | )); 90 | ``` 91 | 92 | #### Default fields 93 | 94 | Every page in the Algolia index will additionally contain an `url` and `intendedTemplate` field that you can later use to display the results. 95 | You can override this behavior or add additional fields to this list by using the `algolia.fields` option: 96 | 97 | ```php 98 | c::set('algolia.fields', array('url', 'intendedTemplate', 'title', 'text')); 99 | ``` 100 | 101 | #### Automatic indexing 102 | 103 | The Algolia plugin contains Panel hooks that will update the index on every page change in the Panel. 104 | Every time you create, update, rename, delete, hide or sort a page, the changes will immediately be reflected in the Algolia index. 105 | 106 | If you only want to use manual indexing, you can disable the automatic indexing with the following option: 107 | 108 | ```php 109 | c::set('algolia.autoindex', false); 110 | ``` 111 | 112 | #### Indexing widget 113 | 114 | The Algolia plugin includes a Panel widget that allows Panel users to manually index the site. You can disable this widget with the following option: 115 | 116 | ```php 117 | c::set('algolia.widget', false); 118 | ``` 119 | 120 | ### Search options 121 | 122 | Algolia has [many search options](https://www.algolia.com/doc/php#full-text-search-parameters) to fine-tune the search results. You can set these in your configuration like this: 123 | 124 | ```php 125 | c::set('algolia.search.options', array( 126 | 'typoTolerance' => false, 127 | 'hitsPerPage' => 50 128 | )); 129 | ``` 130 | 131 | Alternatively, you can also set the options in the display settings of the index in the Algolia dashboard. 132 | 133 | ## Indexing 134 | 135 | The plugin will automatically update the Algolia index with changes you make in the Panel. 136 | If you don't use the Panel or have a custom deployment strategy, you can instead manually index the whole site: 137 | 138 | ```php 139 | algolia()->index(); 140 | ``` 141 | 142 | This will create a new temporary index, upload all indexable pages and replace the main index with the temporary index. 143 | Please note that manual indexing will use roughly as many Algolia "Operations" as you have indexable pages each time you call the `index` method. The amount of included/free "Operations" per month depends on your Algolia plan. 144 | 145 | There is also a Panel widget for this that is enabled by default. 146 | 147 | ## Search 148 | 149 | The plugin also provides a method to query/search the index from the backend. It is generally recommended to use [Algolia's JavaScript library](https://www.algolia.com/doc/javascript#quick-start) to avoid the round-trip to your server, but you should also have a server-side fallback results page, which you can implement using the `search` method: 150 | 151 | ```php 152 | $results = algolia()->search($query, $page = 1, $options = array()); 153 | ``` 154 | 155 | The `$options` array can be used to override your default values in the `algolia.search.options` option. 156 | 157 | **Note**: The `$page` parameter starts at `1` while Algolia uses "zero based" pagination (where the parameter starts at `0`). The plugin converts between these formats automatically to allow you to use Kirby's collection pagination. 158 | 159 | ### Getting metadata about the results 160 | 161 | Algolia returns metadata together with the results. You can get this data from the results collection: 162 | 163 | ```php 164 | // Total count of results 165 | echo 'There are ' . $results->totalCount() . ' results.'; 166 | 167 | // Algolia server processing time in ms 168 | echo 'Processing time: ' . $results->processingTime(); 169 | 170 | // Search query 171 | echo 'You searched for ' . esc($results->query()) . '.'; 172 | 173 | // Algolia search parameter string 174 | // Useful when debugging search requests 175 | echo 'Search params: ' . $results->params(); 176 | ``` 177 | 178 | ### Example 179 | 180 | *Adapted from the [Kirby search example](https://getkirby.com/docs/solutions/search).* 181 | 182 | **`site/controllers/search.php`** 183 | 184 | ```php 185 | search($query, $page); 194 | $pagination = $results->pagination(); 195 | } else { 196 | $results = array(); 197 | $pagination = null; 198 | } 199 | 200 | return compact('results', 'pagination', 'query'); 201 | 202 | }; 203 | ``` 204 | 205 | **`site/templates/search.php`** 206 | 207 | ```php 208 | 209 | 210 |
211 | 212 | 213 |
214 | 215 | 224 | 225 | hasPages()): ?> 226 | 237 | 238 | 239 | 240 | ``` 241 | 242 | ## License 243 | 244 | 245 | 246 | ## Author 247 | 248 | Lukas Bestle 249 | -------------------------------------------------------------------------------- /vendor/algolia-client/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Algolia 4 | http://www.algolia.com/ 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /vendor/algolia-client/algoliasearch.php: -------------------------------------------------------------------------------- 1 | caInfoPath = __DIR__.'/../../resources/ca-bundle.crt'; 87 | foreach ($options as $option => $value) { 88 | switch ($option) { 89 | case self::CAINFO: 90 | $this->caInfoPath = $value; 91 | break; 92 | case self::CURLOPT: 93 | $this->curlOptions = $this->checkCurlOptions($value); 94 | break; 95 | case self::PLACES_ENABLED: 96 | $this->placesEnabled = (bool) $value; 97 | break; 98 | case self::FAILING_HOSTS_CACHE: 99 | if (! $value instanceof FailingHostsCache) { 100 | throw new \InvalidArgumentException('failingHostsCache must be an instance of \AlgoliaSearch\FailingHostsCache.'); 101 | } 102 | break; 103 | default: 104 | throw new \Exception('Unknown option: '.$option); 105 | } 106 | } 107 | 108 | $failingHostsCache = isset($options[self::FAILING_HOSTS_CACHE]) ? $options[self::FAILING_HOSTS_CACHE] : null; 109 | $this->context = new ClientContext($applicationID, $apiKey, $hostsArray, $this->placesEnabled, $failingHostsCache); 110 | } 111 | 112 | /** 113 | * Release curl handle. 114 | */ 115 | public function __destruct() 116 | { 117 | } 118 | 119 | /** 120 | * Change the default connect timeout of 1s to a custom value 121 | * (only useful if your server has a very slow connectivity to Algolia backend). 122 | * 123 | * @param int $connectTimeout the connection timeout 124 | * @param int $timeout the read timeout for the query 125 | * @param int $searchTimeout the read timeout used for search queries only 126 | * 127 | * @throws AlgoliaException 128 | */ 129 | public function setConnectTimeout($connectTimeout, $timeout = 30, $searchTimeout = 5) 130 | { 131 | $version = curl_version(); 132 | $isPhpOld = version_compare(phpversion(), '5.2.3', '<'); 133 | $isCurlOld = version_compare($version['version'], '7.16.2', '<'); 134 | 135 | if (($isPhpOld || $isCurlOld) && $this->context->connectTimeout < 1) { 136 | throw new AlgoliaException( 137 | "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" 138 | ); 139 | } 140 | $this->context->connectTimeout = $connectTimeout; 141 | $this->context->readTimeout = $timeout; 142 | $this->context->searchTimeout = $searchTimeout; 143 | } 144 | 145 | /** 146 | * Allow to use IP rate limit when you have a proxy between end-user and Algolia. 147 | * This option will set the X-Forwarded-For HTTP header with the client IP 148 | * and the X-Forwarded-API-Key with the API Key having rate limits. 149 | * 150 | * @param string $adminAPIKey the admin API Key you can find in your dashboard 151 | * @param string $endUserIP the end user IP (you can use both IPV4 or IPV6 syntax) 152 | * @param string $rateLimitAPIKey the API key on which you have a rate limit 153 | */ 154 | public function enableRateLimitForward($adminAPIKey, $endUserIP, $rateLimitAPIKey) 155 | { 156 | $this->context->setRateLimit($adminAPIKey, $endUserIP, $rateLimitAPIKey); 157 | } 158 | 159 | /** 160 | * The aggregation of the queries to retrieve the latest query 161 | * uses the IP or the user token to work efficiently. 162 | * If the queries are made from your backend server, 163 | * the IP will be the same for all of the queries. 164 | * We're supporting the following HTTP header to forward the IP of your end-user 165 | * to the engine, you just need to set it for each query. 166 | * 167 | * @see https://www.algolia.com/doc/faq/analytics/will-the-analytics-still-work-if-i-perform-the-search-through-my-backend 168 | * 169 | * @param string $ip 170 | */ 171 | public function setForwardedFor($ip) 172 | { 173 | $this->context->setForwardedFor($ip); 174 | } 175 | 176 | /** 177 | * It's possible to use the following token to track users that have the same IP 178 | * or to track users that use different devices. 179 | * 180 | * @see https://www.algolia.com/doc/faq/analytics/will-the-analytics-still-work-if-i-perform-the-search-through-my-backend 181 | * 182 | * @param string $token 183 | */ 184 | public function setAlgoliaUserToken($token) 185 | { 186 | $this->context->setAlgoliaUserToken($token); 187 | } 188 | 189 | /** 190 | * Disable IP rate limit enabled with enableRateLimitForward() function. 191 | */ 192 | public function disableRateLimitForward() 193 | { 194 | $this->context->disableRateLimit(); 195 | } 196 | 197 | /** 198 | * Call isAlive. 199 | */ 200 | public function isAlive() 201 | { 202 | $this->request( 203 | $this->context, 204 | 'GET', 205 | '/1/isalive', 206 | null, 207 | null, 208 | $this->context->readHostsArray, 209 | $this->context->connectTimeout, 210 | $this->context->readTimeout 211 | ); 212 | } 213 | 214 | /** 215 | * Allow to set custom headers. 216 | * 217 | * @param string $key 218 | * @param string $value 219 | */ 220 | public function setExtraHeader($key, $value) 221 | { 222 | $this->context->setExtraHeader($key, $value); 223 | } 224 | 225 | /** 226 | * This method allows to query multiple indexes with one API call. 227 | * 228 | * @param array $queries 229 | * @param string $indexNameKey 230 | * @param string $strategy 231 | * @param array $requestHeaders 232 | * 233 | * @return mixed 234 | * 235 | * @throws AlgoliaException 236 | * @throws \Exception 237 | */ 238 | public function multipleQueries($queries, $indexNameKey = 'indexName', $strategy = 'none') 239 | { 240 | $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); 241 | 242 | if ($queries == null) { 243 | throw new \Exception('No query provided'); 244 | } 245 | $requests = array(); 246 | foreach ($queries as $query) { 247 | if (array_key_exists($indexNameKey, $query)) { 248 | $indexes = $query[$indexNameKey]; 249 | unset($query[$indexNameKey]); 250 | } else { 251 | throw new \Exception('indexName is mandatory'); 252 | } 253 | $req = array('indexName' => $indexes, 'params' => $this->buildQuery($query)); 254 | 255 | array_push($requests, $req); 256 | } 257 | 258 | return $this->request( 259 | $this->context, 260 | 'POST', 261 | '/1/indexes/*/queries', 262 | array(), 263 | array('requests' => $requests, 'strategy' => $strategy), 264 | $this->context->readHostsArray, 265 | $this->context->connectTimeout, 266 | $this->context->searchTimeout, 267 | $requestHeaders 268 | ); 269 | } 270 | 271 | /** 272 | * List all existing indexes 273 | * return an object in the form: 274 | * array( 275 | * "items" => array( 276 | * array("name" => "contacts", "createdAt" => "2013-01-18T15:33:13.556Z"), 277 | * array("name" => "notes", "createdAt" => "2013-01-18T15:33:13.556Z") 278 | * ) 279 | * ). 280 | * 281 | * @return mixed 282 | * 283 | * @throws AlgoliaException 284 | */ 285 | public function listIndexes() 286 | { 287 | $requestHeaders = func_num_args() === 1 && is_array(func_get_arg(0)) ? func_get_arg(0) : array(); 288 | 289 | return $this->request( 290 | $this->context, 291 | 'GET', 292 | '/1/indexes/', 293 | null, 294 | null, 295 | $this->context->readHostsArray, 296 | $this->context->connectTimeout, 297 | $this->context->readTimeout, 298 | $requestHeaders 299 | ); 300 | } 301 | 302 | /** 303 | * Delete an index. 304 | * 305 | * @param string $indexName the name of index to delete 306 | * 307 | * @return mixed an object containing a "deletedAt" attribute 308 | */ 309 | public function deleteIndex($indexName) 310 | { 311 | $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); 312 | 313 | return $this->request( 314 | $this->context, 315 | 'DELETE', 316 | '/1/indexes/'.urlencode($indexName), 317 | null, 318 | null, 319 | $this->context->writeHostsArray, 320 | $this->context->connectTimeout, 321 | $this->context->readTimeout, 322 | $requestHeaders 323 | ); 324 | } 325 | 326 | /** 327 | * Move an existing index. 328 | * 329 | * @param string $srcIndexName the name of index to copy. 330 | * @param string $dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overwritten 331 | * if it already exist). 332 | * 333 | * @return mixed 334 | */ 335 | public function moveIndex($srcIndexName, $dstIndexName) 336 | { 337 | $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); 338 | 339 | $request = array('operation' => 'move', 'destination' => $dstIndexName); 340 | 341 | return $this->request( 342 | $this->context, 343 | 'POST', 344 | '/1/indexes/'.urlencode($srcIndexName).'/operation', 345 | array(), 346 | $request, 347 | $this->context->writeHostsArray, 348 | $this->context->connectTimeout, 349 | $this->context->readTimeout, 350 | $requestHeaders 351 | ); 352 | } 353 | 354 | /** 355 | * Copy an existing index. 356 | * 357 | * @param string $srcIndexName the name of index to copy. 358 | * @param string $dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overwritten 359 | * if it already exist). 360 | * 361 | * @return mixed 362 | */ 363 | public function copyIndex($srcIndexName, $dstIndexName) 364 | { 365 | $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); 366 | 367 | return $this->scopedCopyIndex($srcIndexName, $dstIndexName, array(), $requestHeaders); 368 | } 369 | 370 | /** 371 | * Copy an existing index and define what to copy along with records: 372 | * - settings 373 | * - synonyms 374 | * - query rules 375 | * 376 | * By default, everything is copied. 377 | * 378 | * @param string $srcIndexName the name of index to copy. 379 | * @param string $dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overwritten if it already exist). 380 | * @param array $scope Resource to copy along with records: 'settings', 'rules', 'synonyms' 381 | * @param array $requestHeaders 382 | * @return mixed 383 | */ 384 | public function scopedCopyIndex($srcIndexName, $dstIndexName, array $scope = array(), array $requestHeaders = array()) 385 | { 386 | $request = array( 387 | 'operation' => 'copy', 388 | 'destination' => $dstIndexName, 389 | ); 390 | 391 | if (! empty($scope)) { 392 | $request['scope'] = $scope; 393 | } 394 | 395 | return $this->request( 396 | $this->context, 397 | 'POST', 398 | '/1/indexes/'.urlencode($srcIndexName).'/operation', 399 | array(), 400 | $request, 401 | $this->context->writeHostsArray, 402 | $this->context->connectTimeout, 403 | $this->context->readTimeout, 404 | $requestHeaders 405 | ); 406 | } 407 | 408 | /** 409 | * Return last logs entries. 410 | * 411 | * @param int $offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry). 412 | * @param int $length Specify the maximum number of entries to retrieve starting at offset. Maximum allowed value: 1000. 413 | * @param mixed $type 414 | * 415 | * @return mixed 416 | * 417 | * @throws AlgoliaException 418 | */ 419 | public function getLogs($offset = 0, $length = 10, $type = 'all') 420 | { 421 | $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); 422 | 423 | if (gettype($type) == 'boolean') { //Old prototype onlyError 424 | if ($type) { 425 | $type = 'error'; 426 | } else { 427 | $type = 'all'; 428 | } 429 | } 430 | 431 | return $this->request( 432 | $this->context, 433 | 'GET', 434 | '/1/logs?offset='.$offset.'&length='.$length.'&type='.$type, 435 | null, 436 | null, 437 | $this->context->writeHostsArray, 438 | $this->context->connectTimeout, 439 | $this->context->readTimeout, 440 | $requestHeaders 441 | ); 442 | } 443 | 444 | /** 445 | * Add a userID to the mapping 446 | * @return an object containing a "updatedAt" attribute 447 | * 448 | * @throws AlgoliaException 449 | */ 450 | public function assignUserID($userID, $clusterName) 451 | { 452 | $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); 453 | $requestHeaders["X-Algolia-User-ID"] = $userID; 454 | 455 | $request = array('cluster' => $clusterName); 456 | 457 | return $this->request( 458 | $this->context, 459 | 'POST', 460 | '/1/clusters/mapping', 461 | null, 462 | $request, 463 | $this->context->writeHostsArray, 464 | $this->context->connectTimeout, 465 | $this->context->readTimeout, 466 | $requestHeaders 467 | ); 468 | } 469 | 470 | /** 471 | * Remove a userID from the mapping 472 | * @return an object containing a "deletedAt" attribute 473 | * 474 | * @throws AlgoliaException 475 | */ 476 | public function removeUserID($userID) 477 | { 478 | $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); 479 | $requestHeaders["X-Algolia-User-ID"] = $userID; 480 | 481 | return $this->request( 482 | $this->context, 483 | 'DELETE', 484 | '/1/clusters/mapping', 485 | null, 486 | null, 487 | $this->context->writeHostsArray, 488 | $this->context->connectTimeout, 489 | $this->context->readTimeout, 490 | $requestHeaders 491 | ); 492 | } 493 | 494 | /** 495 | * List available cluster in the mapping 496 | * return an object in the form: 497 | * array( 498 | * "clusters" => array( 499 | * array("clusterName" => "name", "nbRecords" => 0, "nbUserIDs" => 0, "dataSize" => 0) 500 | * ) 501 | * ). 502 | * 503 | * @return mixed 504 | * @throws AlgoliaException 505 | */ 506 | public function listClusters() 507 | { 508 | $requestHeaders = func_num_args() === 1 && is_array(func_get_arg(0)) ? func_get_arg(0) : array(); 509 | 510 | return $this->request( 511 | $this->context, 512 | 'GET', 513 | '/1/clusters', 514 | null, 515 | null, 516 | $this->context->readHostsArray, 517 | $this->context->connectTimeout, 518 | $this->context->readTimeout, 519 | $requestHeaders 520 | ); 521 | } 522 | 523 | /** 524 | * Get one userID in the mapping 525 | * return an object in the form: 526 | * array( 527 | * "userID" => "userName", 528 | * "clusterName" => "name", 529 | * "nbRecords" => 0, 530 | * "dataSize" => 0 531 | * ). 532 | * 533 | * @return mixed 534 | * @throws AlgoliaException 535 | */ 536 | public function getUserID($userID) 537 | { 538 | $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); 539 | 540 | return $this->request( 541 | $this->context, 542 | 'GET', 543 | '/1/clusters/mapping/'.urlencode($userID), 544 | null, 545 | null, 546 | $this->context->readHostsArray, 547 | $this->context->connectTimeout, 548 | $this->context->readTimeout, 549 | $requestHeaders 550 | ); 551 | } 552 | 553 | /** 554 | * List userIDs in the mapping 555 | * return an object in the form: 556 | * array( 557 | * "userIDs" => array( 558 | * array("userID" => "userName", "clusterName" => "name", "nbRecords" => 0, "dataSize" => 0) 559 | * ), 560 | * "page" => 0, 561 | * "hitsPerPage" => 20 562 | * ). 563 | * 564 | * @return mixed 565 | * @throws AlgoliaException 566 | */ 567 | public function listUserIDs($page = 0, $hitsPerPage = 20) 568 | { 569 | $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); 570 | 571 | return $this->request( 572 | $this->context, 573 | 'GET', 574 | '/1/clusters/mapping?page='.$page.'&hitsPerPage='.$hitsPerPage, 575 | null, 576 | null, 577 | $this->context->readHostsArray, 578 | $this->context->connectTimeout, 579 | $this->context->readTimeout, 580 | $requestHeaders 581 | ); 582 | } 583 | 584 | /** 585 | * Get top userID in the mapping 586 | * return an object in the form: 587 | * array( 588 | * "topUsers" => array( 589 | * "clusterName" => array( 590 | * array("userID" => "userName", "nbRecords" => 0, "dataSize" => 0) 591 | * ) 592 | * ) 593 | * ). 594 | * 595 | * @return mixed 596 | * @throws AlgoliaException 597 | */ 598 | public function getTopUserID() 599 | { 600 | $requestHeaders = func_num_args() === 1 && is_array(func_get_arg(0)) ? func_get_arg(0) : array(); 601 | 602 | return $this->request( 603 | $this->context, 604 | 'GET', 605 | '/1/clusters/mapping/top', 606 | null, 607 | null, 608 | $this->context->readHostsArray, 609 | $this->context->connectTimeout, 610 | $this->context->readTimeout, 611 | $requestHeaders 612 | ); 613 | } 614 | 615 | /** 616 | * Search userIDs in the mapping 617 | * return an object in the form: 618 | * array( 619 | * "hits" => array( 620 | * array("userID" => "userName", "clusterName" => "name", "nbRecords" => 0, "dataSize" => 0) 621 | * ), 622 | * "nbHits" => 0 623 | * "page" => 0, 624 | * "hitsPerPage" => 20 625 | * ). 626 | * 627 | * @return mixed 628 | * @throws AlgoliaException 629 | */ 630 | public function searchUserIDs($query, $clusterName = null, $page = null, $hitsPerPage = null) 631 | { 632 | $requestHeaders = func_num_args() === 5 && is_array(func_get_arg(4)) ? func_get_arg(4) : array(); 633 | 634 | $params = array(); 635 | 636 | if ($query !== null) { 637 | $params['query'] = $query; 638 | } 639 | 640 | if ($clusterName !== null) { 641 | $params['cluster'] = $clusterName; 642 | } 643 | 644 | if ($page !== null) { 645 | $params['page'] = $page; 646 | } 647 | 648 | if ($hitsPerPage !== null) { 649 | $params['hitsPerPage'] = $hitsPerPage; 650 | } 651 | 652 | return $this->request( 653 | $this->context, 654 | 'POST', 655 | '/1/clusters/mapping/search', 656 | null, 657 | $params, 658 | $this->context->readHostsArray, 659 | $this->context->connectTimeout, 660 | $this->context->readTimeout, 661 | $requestHeaders 662 | ); 663 | } 664 | 665 | /** 666 | * Get the index object initialized (no server call needed for initialization). 667 | * 668 | * @param string $indexName the name of index 669 | * 670 | * @return Index 671 | * 672 | * @throws AlgoliaException 673 | */ 674 | public function initIndex($indexName) 675 | { 676 | if (empty($indexName)) { 677 | throw new AlgoliaException('Invalid index name: empty string'); 678 | } 679 | 680 | return new Index($this->context, $this, $indexName); 681 | } 682 | 683 | /** 684 | * List all existing API keys with their associated ACLs. 685 | * 686 | * @return mixed 687 | * 688 | * @throws AlgoliaException 689 | */ 690 | public function listApiKeys() 691 | { 692 | $requestHeaders = func_num_args() === 1 && is_array(func_get_arg(0)) ? func_get_arg(0) : array(); 693 | 694 | return $this->request( 695 | $this->context, 696 | 'GET', 697 | '/1/keys', 698 | null, 699 | null, 700 | $this->context->readHostsArray, 701 | $this->context->connectTimeout, 702 | $this->context->readTimeout, 703 | $requestHeaders 704 | ); 705 | } 706 | 707 | /** 708 | * @return mixed 709 | * @deprecated use listApiKeys instead 710 | */ 711 | public function listUserKeys() 712 | { 713 | return $this->listApiKeys(); 714 | } 715 | 716 | /** 717 | * Get ACL of a API key. 718 | * 719 | * @param string $key 720 | * 721 | * @return mixed 722 | */ 723 | public function getApiKey($key) 724 | { 725 | $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); 726 | 727 | return $this->request( 728 | $this->context, 729 | 'GET', 730 | '/1/keys/'.$key, 731 | null, 732 | null, 733 | $this->context->readHostsArray, 734 | $this->context->connectTimeout, 735 | $this->context->readTimeout, 736 | $requestHeaders 737 | ); 738 | } 739 | 740 | /** 741 | * @param $key 742 | * @return mixed 743 | * @deprecated use getApiKey instead 744 | */ 745 | public function getUserKeyACL($key) 746 | { 747 | return $this->getApiKey($key); 748 | } 749 | 750 | /** 751 | * Delete an existing API key. 752 | * 753 | * @param string $key 754 | * 755 | * @return mixed 756 | */ 757 | public function deleteApiKey($key) 758 | { 759 | $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); 760 | 761 | return $this->request( 762 | $this->context, 763 | 'DELETE', 764 | '/1/keys/'.$key, 765 | null, 766 | null, 767 | $this->context->writeHostsArray, 768 | $this->context->connectTimeout, 769 | $this->context->readTimeout, 770 | $requestHeaders 771 | ); 772 | } 773 | 774 | /** 775 | * @param $key 776 | * @return mixed 777 | * @deprecated use deleteApiKey instead 778 | */ 779 | public function deleteUserKey($key) 780 | { 781 | return $this->deleteApiKey($key); 782 | } 783 | 784 | /** 785 | * Create a new API key. 786 | * 787 | * @param array $obj can be two different parameters: 788 | * The list of parameters for this key. Defined by an array that 789 | * can contain the following values: 790 | * - acl: array of string 791 | * - indices: array of string 792 | * - validity: int 793 | * - referers: array of string 794 | * - description: string 795 | * - maxHitsPerQuery: integer 796 | * - queryParameters: string 797 | * - maxQueriesPerIPPerHour: integer 798 | * Or the list of ACL for this key. Defined by an array of string that 799 | * can contains the following values: 800 | * - search: allow to search (https and http) 801 | * - addObject: allows to add/update an object in the index (https only) 802 | * - deleteObject : allows to delete an existing object (https only) 803 | * - deleteIndex : allows to delete index content (https only) 804 | * - settings : allows to get index settings (https only) 805 | * - editSettings : allows to change index settings (https only) 806 | * @param int $validity the number of seconds after which the key will be automatically removed (0 means 807 | * no time limit for this key) 808 | * @param int $maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. 809 | * Defaults to 0 (no rate limit). 810 | * @param int $maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. 811 | * Defaults to 0 (unlimited) 812 | * @param array|null $indexes Specify the list of indices to target (null means all) 813 | * 814 | * @return mixed 815 | * 816 | * @throws AlgoliaException 817 | */ 818 | public function addApiKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0, $indexes = null) 819 | { 820 | $requestHeaders = func_num_args() === 6 && is_array(func_get_arg(5)) ? func_get_arg(5) : array(); 821 | 822 | if ($obj !== array_values($obj)) { 823 | // if $obj doesn't have required entries, we add the default values 824 | $params = $obj; 825 | if ($validity != 0) { 826 | $params['validity'] = $validity; 827 | } 828 | if ($maxQueriesPerIPPerHour != 0) { 829 | $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour; 830 | } 831 | if ($maxHitsPerQuery != 0) { 832 | $params['maxHitsPerQuery'] = $maxHitsPerQuery; 833 | } 834 | } else { 835 | $params = array( 836 | 'acl' => $obj, 837 | 'validity' => $validity, 838 | 'maxQueriesPerIPPerHour' => $maxQueriesPerIPPerHour, 839 | 'maxHitsPerQuery' => $maxHitsPerQuery, 840 | ); 841 | } 842 | 843 | if ($indexes != null) { 844 | $params['indexes'] = $indexes; 845 | } 846 | 847 | return $this->request( 848 | $this->context, 849 | 'POST', 850 | '/1/keys', 851 | array(), 852 | $params, 853 | $this->context->writeHostsArray, 854 | $this->context->connectTimeout, 855 | $this->context->readTimeout, 856 | $requestHeaders 857 | ); 858 | } 859 | 860 | /** 861 | * @param $obj 862 | * @param int $validity 863 | * @param int $maxQueriesPerIPPerHour 864 | * @param int $maxHitsPerQuery 865 | * @param null $indexes 866 | * @return mixed 867 | * @deprecated use addApiKey instead 868 | */ 869 | public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0, $indexes = null) 870 | { 871 | return $this->addApiKey($obj, $validity, $maxQueriesPerIPPerHour, $maxHitsPerQuery, $indexes); 872 | } 873 | 874 | /** 875 | * Update an API key. 876 | * 877 | * @param string $key 878 | * @param array $obj can be two different parameters: 879 | * The list of parameters for this key. Defined by a array that 880 | * can contains the following values: 881 | * - acl: array of string 882 | * - indices: array of string 883 | * - validity: int 884 | * - referers: array of string 885 | * - description: string 886 | * - maxHitsPerQuery: integer 887 | * - queryParameters: string 888 | * - maxQueriesPerIPPerHour: integer 889 | * Or the list of ACL for this key. Defined by an array of string that 890 | * can contains the following values: 891 | * - search: allow to search (https and http) 892 | * - addObject: allows to add/update an object in the index (https only) 893 | * - deleteObject : allows to delete an existing object (https only) 894 | * - deleteIndex : allows to delete index content (https only) 895 | * - settings : allows to get index settings (https only) 896 | * - editSettings : allows to change index settings (https only) 897 | * @param int $validity the number of seconds after which the key will be automatically removed (0 means 898 | * no time limit for this key) 899 | * @param int $maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. 900 | * Defaults to 0 (no rate limit). 901 | * @param int $maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. Defaults 902 | * to 0 (unlimited) 903 | * @param array|null $indexes Specify the list of indices to target (null means all) 904 | * 905 | * @return mixed 906 | * 907 | * @throws AlgoliaException 908 | */ 909 | public function updateApiKey( 910 | $key, 911 | $obj, 912 | $validity = 0, 913 | $maxQueriesPerIPPerHour = 0, 914 | $maxHitsPerQuery = 0, 915 | $indexes = null 916 | ) { 917 | $requestHeaders = func_num_args() === 7 && is_array(func_get_arg(6)) ? func_get_arg(6) : array(); 918 | 919 | if ($obj !== array_values($obj)) { // is dict of value 920 | $params = $obj; 921 | $params['validity'] = $validity; 922 | $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour; 923 | $params['maxHitsPerQuery'] = $maxHitsPerQuery; 924 | } else { 925 | $params = array( 926 | 'acl' => $obj, 927 | 'validity' => $validity, 928 | 'maxQueriesPerIPPerHour' => $maxQueriesPerIPPerHour, 929 | 'maxHitsPerQuery' => $maxHitsPerQuery, 930 | ); 931 | } 932 | if ($indexes != null) { 933 | $params['indexes'] = $indexes; 934 | } 935 | 936 | return $this->request( 937 | $this->context, 938 | 'PUT', 939 | '/1/keys/'.$key, 940 | array(), 941 | $params, 942 | $this->context->writeHostsArray, 943 | $this->context->connectTimeout, 944 | $this->context->readTimeout, 945 | $requestHeaders 946 | ); 947 | } 948 | 949 | /** 950 | * @param $key 951 | * @param $obj 952 | * @param int $validity 953 | * @param int $maxQueriesPerIPPerHour 954 | * @param int $maxHitsPerQuery 955 | * @param null $indexes 956 | * @return mixed 957 | * @deprecated use updateApiKey instead 958 | */ 959 | public function updateUserKey( 960 | $key, 961 | $obj, 962 | $validity = 0, 963 | $maxQueriesPerIPPerHour = 0, 964 | $maxHitsPerQuery = 0, 965 | $indexes = null 966 | ) { 967 | $requestHeaders = func_num_args() === 7 && is_array(func_get_arg(6)) ? func_get_arg(6) : array(); 968 | 969 | return $this->updateApiKey($key, $obj, $validity, $maxQueriesPerIPPerHour, $maxHitsPerQuery, $indexes, $requestHeaders); 970 | } 971 | 972 | /** 973 | * Send a batch request targeting multiple indices. 974 | * 975 | * @param array $requests an associative array defining the batch request body 976 | * @param array $requestHeaders 977 | * 978 | * @return mixed 979 | */ 980 | public function batch($requests) 981 | { 982 | $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); 983 | 984 | return $this->request( 985 | $this->context, 986 | 'POST', 987 | '/1/indexes/*/batch', 988 | array(), 989 | array('requests' => $requests), 990 | $this->context->writeHostsArray, 991 | $this->context->connectTimeout, 992 | $this->context->readTimeout, 993 | $requestHeaders 994 | ); 995 | } 996 | 997 | /** 998 | * Generate a secured and public API Key from a list of query parameters and an 999 | * optional user token identifying the current user. 1000 | * 1001 | * @param string $privateApiKey your private API Key 1002 | * @param mixed $query the list of query parameters applied to the query (used as security) 1003 | * @param string|null $userToken an optional token identifying the current user 1004 | * 1005 | * @return string 1006 | */ 1007 | public static function generateSecuredApiKey($privateApiKey, $query, $userToken = null) 1008 | { 1009 | if (is_array($query)) { 1010 | $queryParameters = array(); 1011 | if (array_keys($query) !== array_keys(array_keys($query))) { 1012 | // array of query parameters 1013 | $queryParameters = $query; 1014 | } else { 1015 | // array of tags 1016 | $tmp = array(); 1017 | foreach ($query as $tag) { 1018 | if (is_array($tag)) { 1019 | array_push($tmp, '('.implode(',', $tag).')'); 1020 | } else { 1021 | array_push($tmp, $tag); 1022 | } 1023 | } 1024 | $tagFilters = implode(',', $tmp); 1025 | $queryParameters['tagFilters'] = $tagFilters; 1026 | } 1027 | if ($userToken != null && strlen($userToken) > 0) { 1028 | $queryParameters['userToken'] = $userToken; 1029 | } 1030 | $urlEncodedQuery = static::buildQuery($queryParameters); 1031 | } else { 1032 | if (strpos($query, '=') === false) { 1033 | // String of tags 1034 | $queryParameters = array('tagFilters' => $query); 1035 | 1036 | if ($userToken != null && strlen($userToken) > 0) { 1037 | $queryParameters['userToken'] = $userToken; 1038 | } 1039 | $urlEncodedQuery = static::buildQuery($queryParameters); 1040 | } else { 1041 | // url encoded query 1042 | $urlEncodedQuery = $query; 1043 | if ($userToken != null && strlen($userToken) > 0) { 1044 | $urlEncodedQuery = $urlEncodedQuery.'&userToken='.urlencode($userToken); 1045 | } 1046 | } 1047 | } 1048 | $content = hash_hmac('sha256', $urlEncodedQuery, $privateApiKey).$urlEncodedQuery; 1049 | 1050 | return base64_encode($content); 1051 | } 1052 | 1053 | /** 1054 | * @param array $args 1055 | * 1056 | * @return string 1057 | */ 1058 | public static function buildQuery($args) 1059 | { 1060 | foreach ($args as $key => $value) { 1061 | if (gettype($value) == 'array') { 1062 | $args[$key] = Json::encode($value); 1063 | } 1064 | } 1065 | 1066 | return http_build_query($args); 1067 | } 1068 | 1069 | /** 1070 | * @param ClientContext $context 1071 | * @param string $method 1072 | * @param string $path 1073 | * @param array $params 1074 | * @param array $data 1075 | * @param array $hostsArray 1076 | * @param int $connectTimeout 1077 | * @param int $readTimeout 1078 | * @param array $requestHeaders 1079 | * 1080 | * @return mixed 1081 | * 1082 | * @throws AlgoliaException 1083 | */ 1084 | public function request( 1085 | $context, 1086 | $method, 1087 | $path, 1088 | $params, 1089 | $data, 1090 | $hostsArray, 1091 | $connectTimeout, 1092 | $readTimeout 1093 | ) { 1094 | $requestHeaders = func_num_args() === 9 && is_array(func_get_arg(8)) ? func_get_arg(8) : array(); 1095 | 1096 | $exceptions = array(); 1097 | $cnt = 0; 1098 | foreach ($hostsArray as &$host) { 1099 | $cnt += 1; 1100 | if ($cnt == 3) { 1101 | $connectTimeout += 2; 1102 | $readTimeout += 10; 1103 | } 1104 | try { 1105 | $res = $this->doRequest($context, $method, $host, $path, $params, $data, $connectTimeout, $readTimeout, $requestHeaders); 1106 | if ($res !== null) { 1107 | return $res; 1108 | } 1109 | } catch (AlgoliaException $e) { 1110 | throw $e; 1111 | } catch (\Exception $e) { 1112 | $exceptions[$host] = $e->getMessage(); 1113 | if ($context instanceof ClientContext) { 1114 | $context->addFailingHost($host); // Needs to be before the rotation otherwise it will not be rotated 1115 | $context->rotateHosts(); 1116 | } 1117 | } 1118 | } 1119 | throw new AlgoliaConnectionException('Hosts unreachable: '.implode(',', $exceptions)); 1120 | } 1121 | 1122 | /** 1123 | * @param ClientContext $context 1124 | * @param string $method 1125 | * @param string $host 1126 | * @param string $path 1127 | * @param array $params 1128 | * @param array $data 1129 | * @param int $connectTimeout 1130 | * @param int $readTimeout 1131 | * @param array $requestHeaders 1132 | * 1133 | * @return mixed 1134 | * 1135 | * @throws AlgoliaException 1136 | * @throws \Exception 1137 | */ 1138 | public function doRequest( 1139 | $context, 1140 | $method, 1141 | $host, 1142 | $path, 1143 | $params, 1144 | $data, 1145 | $connectTimeout, 1146 | $readTimeout 1147 | ) { 1148 | $requestHeaders = func_num_args() === 9 && is_array(func_get_arg(8)) ? func_get_arg(8) : array(); 1149 | 1150 | if (strpos($host, 'http') === 0) { 1151 | $url = $host.$path; 1152 | } else { 1153 | $url = 'https://'.$host.$path; 1154 | } 1155 | 1156 | if ($params != null && count($params) > 0) { 1157 | $params2 = array(); 1158 | foreach ($params as $key => $val) { 1159 | if (is_array($val)) { 1160 | $params2[$key] = Json::encode($val); 1161 | } else { 1162 | $params2[$key] = $val; 1163 | } 1164 | } 1165 | $url .= '?'.http_build_query($params2); 1166 | } 1167 | 1168 | // initialize curl library 1169 | $curlHandle = curl_init(); 1170 | 1171 | // set curl options 1172 | try { 1173 | foreach ($this->curlOptions as $curlOption => $optionValue) { 1174 | curl_setopt($curlHandle, constant($curlOption), $optionValue); 1175 | } 1176 | } catch (\Exception $e) { 1177 | $this->invalidOptions($this->curlOptions, $e->getMessage()); 1178 | } 1179 | 1180 | //curl_setopt($curlHandle, CURLOPT_VERBOSE, true); 1181 | 1182 | $defaultHeaders = null; 1183 | if ($context->adminAPIKey == null) { 1184 | $defaultHeaders = array( 1185 | 'X-Algolia-Application-Id' => $context->applicationID, 1186 | 'X-Algolia-API-Key' => $context->apiKey, 1187 | 'Content-type' => 'application/json', 1188 | ); 1189 | } else { 1190 | $defaultHeaders = array( 1191 | 'X-Algolia-Application-Id' => $context->applicationID, 1192 | 'X-Algolia-API-Key' => $context->adminAPIKey, 1193 | 'X-Forwarded-For' => $context->endUserIP, 1194 | 'X-Algolia-UserToken' => $context->algoliaUserToken, 1195 | 'X-Forwarded-API-Key' => $context->rateLimitAPIKey, 1196 | 'Content-type' => 'application/json', 1197 | ); 1198 | } 1199 | 1200 | $headers = array_merge($defaultHeaders, $context->headers, $requestHeaders); 1201 | 1202 | $curlHeaders = array(); 1203 | foreach ($headers as $key => $value) { 1204 | $curlHeaders[] = $key.': '.$value; 1205 | } 1206 | 1207 | curl_setopt($curlHandle, CURLOPT_HTTPHEADER, $curlHeaders); 1208 | 1209 | curl_setopt($curlHandle, CURLOPT_USERAGENT, Version::getUserAgent()); 1210 | //Return the output instead of printing it 1211 | curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); 1212 | curl_setopt($curlHandle, CURLOPT_FAILONERROR, true); 1213 | curl_setopt($curlHandle, CURLOPT_ENCODING, ''); 1214 | curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, true); 1215 | curl_setopt($curlHandle, CURLOPT_SSL_VERIFYHOST, 2); 1216 | curl_setopt($curlHandle, CURLOPT_CAINFO, $this->caInfoPath); 1217 | 1218 | curl_setopt($curlHandle, CURLOPT_URL, $url); 1219 | $version = curl_version(); 1220 | if (version_compare(phpversion(), '5.2.3', '>=') && version_compare($version['version'], '7.16.2', '>=') && $connectTimeout < 1) { 1221 | curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT_MS, $connectTimeout * 1000); 1222 | curl_setopt($curlHandle, CURLOPT_TIMEOUT_MS, $readTimeout * 1000); 1223 | } else { 1224 | curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, $connectTimeout); 1225 | curl_setopt($curlHandle, CURLOPT_TIMEOUT, $readTimeout); 1226 | } 1227 | 1228 | // The problem is that on (Li|U)nix, when libcurl uses the standard name resolver, 1229 | // a SIGALRM is raised during name resolution which libcurl thinks is the timeout alarm. 1230 | curl_setopt($curlHandle, CURLOPT_NOSIGNAL, 1); 1231 | curl_setopt($curlHandle, CURLOPT_FAILONERROR, false); 1232 | 1233 | if ($method === 'GET') { 1234 | curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'GET'); 1235 | curl_setopt($curlHandle, CURLOPT_HTTPGET, true); 1236 | curl_setopt($curlHandle, CURLOPT_POST, false); 1237 | } else { 1238 | if ($method === 'POST') { 1239 | $body = ($data) ? Json::encode($data) : ''; 1240 | curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'POST'); 1241 | curl_setopt($curlHandle, CURLOPT_POST, true); 1242 | curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body); 1243 | } elseif ($method === 'DELETE') { 1244 | curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'DELETE'); 1245 | curl_setopt($curlHandle, CURLOPT_POST, false); 1246 | } elseif ($method === 'PUT') { 1247 | $body = ($data) ? Json::encode($data) : ''; 1248 | curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'PUT'); 1249 | curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body); 1250 | curl_setopt($curlHandle, CURLOPT_POST, true); 1251 | } 1252 | } 1253 | $mhandle = $context->getMHandle($curlHandle); 1254 | 1255 | // Do all the processing. 1256 | $running = null; 1257 | do { 1258 | $mrc = curl_multi_exec($mhandle, $running); 1259 | } while ($mrc == CURLM_CALL_MULTI_PERFORM); 1260 | 1261 | while ($running && $mrc == CURLM_OK) { 1262 | if (curl_multi_select($mhandle, 0.1) == -1) { 1263 | usleep(100); 1264 | } 1265 | do { 1266 | $mrc = curl_multi_exec($mhandle, $running); 1267 | } while ($mrc == CURLM_CALL_MULTI_PERFORM); 1268 | } 1269 | 1270 | $http_status = (int) curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); 1271 | $response = curl_multi_getcontent($curlHandle); 1272 | $error = curl_error($curlHandle); 1273 | 1274 | if (!empty($error)) { 1275 | throw new \Exception($error); 1276 | } 1277 | 1278 | if ($http_status === 0 || $http_status === 503) { 1279 | // Could not reach host or service unavailable, try with another one if we have it 1280 | $context->releaseMHandle($curlHandle); 1281 | curl_close($curlHandle); 1282 | 1283 | return; 1284 | } 1285 | 1286 | $answer = Json::decode($response, true); 1287 | $context->releaseMHandle($curlHandle); 1288 | curl_close($curlHandle); 1289 | 1290 | if (intval($http_status / 100) == 4) { 1291 | throw new AlgoliaException(isset($answer['message']) ? $answer['message'] : $http_status.' error', $http_status); 1292 | } elseif (intval($http_status / 100) != 2) { 1293 | throw new \Exception($http_status.': '.$response, $http_status); 1294 | } 1295 | 1296 | return $answer; 1297 | } 1298 | 1299 | /** 1300 | * Checks if curl option passed are valid curl options. 1301 | * 1302 | * @param array $curlOptions must be array but no type required while first test throw clear Exception 1303 | * 1304 | * @return array 1305 | */ 1306 | protected function checkCurlOptions($curlOptions) 1307 | { 1308 | if (!is_array($curlOptions)) { 1309 | throw new \InvalidArgumentException( 1310 | sprintf( 1311 | 'AlgoliaSearch requires %s option to be array of valid curl options.', 1312 | static::CURLOPT 1313 | ) 1314 | ); 1315 | } 1316 | 1317 | $checkedCurlOptions = array_intersect(array_keys($curlOptions), array_keys($this->getCurlConstants())); 1318 | 1319 | if (count($checkedCurlOptions) !== count($curlOptions)) { 1320 | $this->invalidOptions($curlOptions); 1321 | } 1322 | 1323 | return $curlOptions; 1324 | } 1325 | 1326 | /** 1327 | * Get all php curl available options. 1328 | * 1329 | * @return array 1330 | */ 1331 | protected function getCurlConstants() 1332 | { 1333 | if (!is_null($this->curlConstants)) { 1334 | return $this->curlConstants; 1335 | } 1336 | 1337 | $curlAllConstants = get_defined_constants(true); 1338 | 1339 | if (isset($curlAllConstants['curl'])) { 1340 | $curlAllConstants = $curlAllConstants['curl']; 1341 | } elseif (isset($curlAllConstants['Core'])) { // hhvm 1342 | $curlAllConstants = $curlAllConstants['Core']; 1343 | } else { 1344 | return $this->curlConstants; 1345 | } 1346 | 1347 | $curlConstants = array(); 1348 | foreach ($curlAllConstants as $constantName => $constantValue) { 1349 | if (strpos($constantName, 'CURLOPT') === 0) { 1350 | $curlConstants[$constantName] = $constantValue; 1351 | } 1352 | } 1353 | 1354 | $this->curlConstants = $curlConstants; 1355 | 1356 | return $this->curlConstants; 1357 | } 1358 | 1359 | /** 1360 | * throw clear Exception when bad curl option is set. 1361 | * 1362 | * @param array $curlOptions 1363 | * @param string $errorMsg add specific message for disambiguation 1364 | */ 1365 | protected function invalidOptions(array $curlOptions = array(), $errorMsg = '') 1366 | { 1367 | throw new \OutOfBoundsException( 1368 | sprintf( 1369 | 'AlgoliaSearch %s options keys are invalid. %s given. error message : %s', 1370 | static::CURLOPT, 1371 | Json::encode($curlOptions), 1372 | $errorMsg 1373 | ) 1374 | ); 1375 | } 1376 | 1377 | /** 1378 | * @return PlacesIndex 1379 | */ 1380 | private function getPlacesIndex() 1381 | { 1382 | return new PlacesIndex($this->context, $this); 1383 | } 1384 | 1385 | /** 1386 | * @param string|null $appId 1387 | * @param string|null $apiKey 1388 | * @param array|null $hostsArray 1389 | * @param array $options 1390 | * 1391 | * @return PlacesIndex 1392 | */ 1393 | public static function initPlaces($appId = null, $apiKey = null, $hostsArray = null, $options = array()) 1394 | { 1395 | $options['placesEnabled'] = true; 1396 | $client = new static($appId, $apiKey, $hostsArray, $options); 1397 | 1398 | return $client->getPlacesIndex(); 1399 | } 1400 | 1401 | public function getContext() 1402 | { 1403 | return $this->context; 1404 | } 1405 | } 1406 | -------------------------------------------------------------------------------- /vendor/algolia-client/src/AlgoliaSearch/ClientContext.php: -------------------------------------------------------------------------------- 1 | connectTimeout = 1; 97 | 98 | // global timeout of 30s by default 99 | $this->readTimeout = 30; 100 | 101 | // search timeout of 5s by default 102 | $this->searchTimeout = 5; 103 | 104 | $this->applicationID = $applicationID; 105 | $this->apiKey = $apiKey; 106 | 107 | $this->readHostsArray = $hostsArray; 108 | $this->writeHostsArray = $hostsArray; 109 | 110 | if ($this->readHostsArray == null || count($this->readHostsArray) == 0) { 111 | $this->readHostsArray = $this->getDefaultReadHosts($placesEnabled); 112 | $this->writeHostsArray = $this->getDefaultWriteHosts(); 113 | } 114 | 115 | if (($this->applicationID == null || mb_strlen($this->applicationID) == 0) && $placesEnabled === false) { 116 | throw new Exception('AlgoliaSearch requires an applicationID.'); 117 | } 118 | 119 | if (($this->apiKey == null || mb_strlen($this->apiKey) == 0) && $placesEnabled === false) { 120 | throw new Exception('AlgoliaSearch requires an apiKey.'); 121 | } 122 | 123 | $this->curlMHandle = null; 124 | $this->adminAPIKey = null; 125 | $this->endUserIP = null; 126 | $this->algoliaUserToken = null; 127 | $this->rateLimitAPIKey = null; 128 | $this->headers = array(); 129 | 130 | if ($failingHostsCache === null) { 131 | $this->failingHostsCache = new InMemoryFailingHostsCache(); 132 | } else { 133 | $this->failingHostsCache = $failingHostsCache; 134 | } 135 | 136 | $this->rotateHosts(); 137 | } 138 | 139 | /** 140 | * @param bool $placesEnabled 141 | * 142 | * @return array 143 | */ 144 | private function getDefaultReadHosts($placesEnabled) 145 | { 146 | if ($placesEnabled) { 147 | $hosts = array( 148 | 'places-1.algolianet.com', 149 | 'places-2.algolianet.com', 150 | 'places-3.algolianet.com', 151 | ); 152 | shuffle($hosts); 153 | array_unshift($hosts, 'places-dsn.algolia.net'); 154 | 155 | return $hosts; 156 | } 157 | 158 | $hosts = array( 159 | $this->applicationID.'-1.algolianet.com', 160 | $this->applicationID.'-2.algolianet.com', 161 | $this->applicationID.'-3.algolianet.com', 162 | ); 163 | shuffle($hosts); 164 | array_unshift($hosts, $this->applicationID.'-dsn.algolia.net'); 165 | 166 | return $hosts; 167 | } 168 | 169 | /** 170 | * @return array 171 | */ 172 | private function getDefaultWriteHosts() 173 | { 174 | $hosts = array( 175 | $this->applicationID.'-1.algolianet.com', 176 | $this->applicationID.'-2.algolianet.com', 177 | $this->applicationID.'-3.algolianet.com', 178 | ); 179 | shuffle($hosts); 180 | array_unshift($hosts, $this->applicationID.'.algolia.net'); 181 | 182 | return $hosts; 183 | } 184 | 185 | /** 186 | * Closes eventually opened curl handles. 187 | */ 188 | public function __destruct() 189 | { 190 | if (is_resource($this->curlMHandle)) { 191 | curl_multi_close($this->curlMHandle); 192 | } 193 | } 194 | 195 | /** 196 | * @param $curlHandle 197 | * 198 | * @return resource 199 | */ 200 | public function getMHandle($curlHandle) 201 | { 202 | if (!is_resource($this->curlMHandle)) { 203 | $this->curlMHandle = curl_multi_init(); 204 | } 205 | curl_multi_add_handle($this->curlMHandle, $curlHandle); 206 | 207 | return $this->curlMHandle; 208 | } 209 | 210 | /** 211 | * @param $curlHandle 212 | */ 213 | public function releaseMHandle($curlHandle) 214 | { 215 | curl_multi_remove_handle($this->curlMHandle, $curlHandle); 216 | } 217 | 218 | /** 219 | * @param string $ip 220 | */ 221 | public function setForwardedFor($ip) 222 | { 223 | $this->endUserIP = $ip; 224 | } 225 | 226 | /** 227 | * @param string $token 228 | */ 229 | public function setAlgoliaUserToken($token) 230 | { 231 | $this->algoliaUserToken = $token; 232 | } 233 | 234 | /** 235 | * @param string $adminAPIKey 236 | * @param string $endUserIP 237 | * @param string $rateLimitAPIKey 238 | */ 239 | public function setRateLimit($adminAPIKey, $endUserIP, $rateLimitAPIKey) 240 | { 241 | $this->adminAPIKey = $adminAPIKey; 242 | $this->endUserIP = $endUserIP; 243 | $this->rateLimitAPIKey = $rateLimitAPIKey; 244 | } 245 | 246 | /** 247 | * Disables the rate limit. 248 | */ 249 | public function disableRateLimit() 250 | { 251 | $this->adminAPIKey = null; 252 | $this->endUserIP = null; 253 | $this->rateLimitAPIKey = null; 254 | } 255 | 256 | /** 257 | * @param string $key 258 | * @param string $value 259 | */ 260 | public function setExtraHeader($key, $value) 261 | { 262 | $this->headers[$key] = $value; 263 | } 264 | 265 | /** 266 | * @param string $host 267 | */ 268 | public function addFailingHost($host) 269 | { 270 | $this->failingHostsCache->addFailingHost($host); 271 | } 272 | 273 | /** 274 | * @return FailingHostsCache 275 | */ 276 | public function getFailingHostsCache() 277 | { 278 | return $this->failingHostsCache; 279 | } 280 | /** 281 | * This method is called to pass on failing hosts. 282 | * If the host is first either in the failingHosts array, we 283 | * rotate the array to ensure the next API call will be directly made with a working 284 | * host. This mainly ensures we don't add the equivalent of the connection timeout value to each 285 | * request to the API. 286 | */ 287 | public function rotateHosts() 288 | { 289 | $failingHosts = $this->failingHostsCache->getFailingHosts(); 290 | $i = 0; 291 | while ($i <= count($this->readHostsArray) && in_array($this->readHostsArray[0], $failingHosts)) { 292 | $i++; 293 | $this->readHostsArray[] = array_shift($this->readHostsArray); 294 | } 295 | 296 | $i = 0; 297 | while ($i <= count($this->writeHostsArray) && in_array($this->writeHostsArray[0], $failingHosts)) { 298 | $i++; 299 | $this->writeHostsArray[] = array_shift($this->writeHostsArray); 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /vendor/algolia-client/src/AlgoliaSearch/FailingHostsCache.php: -------------------------------------------------------------------------------- 1 | failingHostsCacheFile = $this->getDefaultCacheFile(); 36 | } else { 37 | $this->failingHostsCacheFile = (string) $file; 38 | } 39 | 40 | $this->assertCacheFileIsValid($this->failingHostsCacheFile); 41 | 42 | if ($ttl === null) { 43 | $ttl = 60 * 5; // 5 minutes 44 | } 45 | 46 | $this->ttl = (int) $ttl; 47 | } 48 | 49 | /** 50 | * @return int 51 | */ 52 | public function getTtl() 53 | { 54 | return $this->ttl; 55 | } 56 | 57 | /** 58 | * @param $file 59 | */ 60 | private function assertCacheFileIsValid($file) 61 | { 62 | $fileDirectory = dirname($file); 63 | 64 | if (! is_writable($fileDirectory)) { 65 | throw new \RuntimeException(sprintf('Cache file directory "%s" is not writable.', $fileDirectory)); 66 | } 67 | 68 | if (! file_exists($file)) { 69 | // The dir being writable, the file will be created when needed. 70 | return; 71 | } 72 | 73 | if (! is_readable($file)) { 74 | throw new \RuntimeException(sprintf('Cache file "%s" is not readable.', $file)); 75 | } 76 | 77 | if (! is_writable($file)) { 78 | throw new \RuntimeException(sprintf('Cache file "%s" is not writable.', $file)); 79 | } 80 | } 81 | 82 | /** 83 | * @return string 84 | */ 85 | private function getDefaultCacheFile() 86 | { 87 | return sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'algolia-failing-hosts'; 88 | } 89 | 90 | /** 91 | * @param string $host 92 | */ 93 | public function addFailingHost($host) 94 | { 95 | $cache = $this->loadFailingHostsCacheFromDisk(); 96 | 97 | if (isset($cache[self::TIMESTAMP]) && isset($cache[self::FAILING_HOSTS])) { 98 | // Update failing hosts cache. 99 | // Here we don't take care of invalidating. We do that on retrieval. 100 | if (!in_array($host, $cache[self::FAILING_HOSTS])) { 101 | $cache[self::FAILING_HOSTS][] = $host; 102 | $this->writeFailingHostsCacheFile($cache); 103 | } 104 | } else { 105 | $cache[self::TIMESTAMP] = time(); 106 | $cache[self::FAILING_HOSTS] = array($host); 107 | $this->writeFailingHostsCacheFile($cache); 108 | } 109 | } 110 | 111 | /** 112 | * Get failing hosts from cache. This method should also handle cache invalidation if required. 113 | * The TTL of the failed hosts cache should be 5mins. 114 | * 115 | * @return array 116 | */ 117 | public function getFailingHosts() 118 | { 119 | $cache = $this->loadFailingHostsCacheFromDisk(); 120 | 121 | return isset($cache[self::FAILING_HOSTS]) ? $cache[self::FAILING_HOSTS] : array(); 122 | } 123 | 124 | /** 125 | * Removes the file storing the failing hosts. 126 | */ 127 | public function flushFailingHostsCache() 128 | { 129 | if (file_exists($this->failingHostsCacheFile)) { 130 | unlink($this->failingHostsCacheFile); 131 | } 132 | } 133 | 134 | /** 135 | * @return array 136 | */ 137 | private function loadFailingHostsCacheFromDisk() 138 | { 139 | if (! file_exists($this->failingHostsCacheFile)) { 140 | return array(); 141 | } 142 | 143 | $json = file_get_contents($this->failingHostsCacheFile); 144 | if ($json === false) { 145 | return array(); 146 | } 147 | 148 | $data = json_decode($json, true); 149 | 150 | if (json_last_error() !== JSON_ERROR_NONE) { 151 | return array(); 152 | } 153 | 154 | // Some basic checks. 155 | if ( 156 | !isset($data[self::TIMESTAMP]) 157 | || !isset($data[self::FAILING_HOSTS]) 158 | || !is_int($data[self::TIMESTAMP]) 159 | || !is_array($data[self::FAILING_HOSTS]) 160 | ) { 161 | return array(); 162 | } 163 | 164 | // Validate the hosts array. 165 | foreach ($data[self::FAILING_HOSTS] as $host) { 166 | if (!is_string($host)) { 167 | return array(); 168 | } 169 | } 170 | 171 | $elapsed = time() - $data[self::TIMESTAMP]; // Number of seconds elapsed. 172 | 173 | if ($elapsed > $this->ttl) { 174 | $this->flushFailingHostsCache(); 175 | 176 | return array(); 177 | } 178 | 179 | return $data; 180 | } 181 | 182 | /** 183 | * @param array $data 184 | */ 185 | private function writeFailingHostsCacheFile(array $data) 186 | { 187 | $json = json_encode($data); 188 | if ($json !== false) { 189 | file_put_contents($this->failingHostsCacheFile, $json); 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /vendor/algolia-client/src/AlgoliaSearch/InMemoryFailingHostsCache.php: -------------------------------------------------------------------------------- 1 | ttl = (int) $ttl; 33 | } 34 | 35 | 36 | /** 37 | * @param string $host 38 | */ 39 | public function addFailingHost($host) 40 | { 41 | if (! in_array($host, self::$failingHosts)) { 42 | // Keep a local cache of failed hosts in case the file based strategy doesn't work out. 43 | self::$failingHosts[] = $host; 44 | 45 | if (self::$timestamp === null) { 46 | self::$timestamp = time(); 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * Get failing hosts from cache. This method should also handle cache invalidation if required. 53 | * The TTL of the failed hosts cache should be 5mins. 54 | * 55 | * @return array 56 | */ 57 | public function getFailingHosts() 58 | { 59 | if (self::$timestamp === null) { 60 | return self::$failingHosts; 61 | } 62 | 63 | $elapsed = time() - self::$timestamp; 64 | if ($elapsed > $this->ttl) { 65 | $this->flushFailingHostsCache(); 66 | } 67 | 68 | return self::$failingHosts; 69 | } 70 | 71 | public function flushFailingHostsCache() 72 | { 73 | self::$failingHosts = array(); 74 | self::$timestamp = null; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /vendor/algolia-client/src/AlgoliaSearch/Index.php: -------------------------------------------------------------------------------- 1 | context = $context; 72 | $this->client = $client; 73 | $this->indexName = $indexName; 74 | $this->urlIndexName = urlencode($indexName); 75 | } 76 | 77 | /** 78 | * Perform batch operation on several objects. 79 | * 80 | * @param array $objects contains an array of objects to update (each object must contains an objectID 81 | * attribute) 82 | * @param string $objectIDKey the key in each object that contains the objectID 83 | * @param string $objectActionKey the key in each object that contains the action to perform (addObject, updateObject, 84 | * deleteObject or partialUpdateObject) 85 | * 86 | * @return mixed 87 | * 88 | * @throws \Exception 89 | */ 90 | public function batchObjects($objects, $objectIDKey = 'objectID', $objectActionKey = 'objectAction') 91 | { 92 | $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); 93 | 94 | $requests = array(); 95 | $allowedActions = array( 96 | 'addObject', 97 | 'updateObject', 98 | 'deleteObject', 99 | 'partialUpdateObject', 100 | 'partialUpdateObjectNoCreate', 101 | ); 102 | 103 | foreach ($objects as $obj) { 104 | // If no or invalid action, assume updateObject 105 | if (!isset($obj[$objectActionKey]) || !in_array($obj[$objectActionKey], $allowedActions)) { 106 | throw new \Exception('invalid or no action detected'); 107 | } 108 | 109 | $action = $obj[$objectActionKey]; 110 | 111 | // The action key is not included in the object 112 | unset($obj[$objectActionKey]); 113 | 114 | $req = array('action' => $action, 'body' => $obj); 115 | 116 | if (array_key_exists($objectIDKey, $obj)) { 117 | $req['objectID'] = (string) $obj[$objectIDKey]; 118 | } 119 | 120 | $requests[] = $req; 121 | } 122 | 123 | return $this->batch(array('requests' => $requests), $requestHeaders); 124 | } 125 | 126 | /** 127 | * Add an object in this index. 128 | * 129 | * @param array $content contains the object to add inside the index. 130 | * The object is represented by an associative array 131 | * @param string|null $objectID (optional) an objectID you want to attribute to this object 132 | * (if the attribute already exist the old object will be overwrite) 133 | * 134 | * @return mixed 135 | */ 136 | public function addObject($content, $objectID = null) 137 | { 138 | $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); 139 | 140 | if ($objectID === null) { 141 | return $this->client->request( 142 | $this->context, 143 | 'POST', 144 | '/1/indexes/'.$this->urlIndexName, 145 | array(), 146 | $content, 147 | $this->context->writeHostsArray, 148 | $this->context->connectTimeout, 149 | $this->context->readTimeout, 150 | $requestHeaders 151 | ); 152 | } 153 | 154 | return $this->client->request( 155 | $this->context, 156 | 'PUT', 157 | '/1/indexes/'.$this->urlIndexName.'/'.urlencode($objectID), 158 | array(), 159 | $content, 160 | $this->context->writeHostsArray, 161 | $this->context->connectTimeout, 162 | $this->context->readTimeout, 163 | $requestHeaders 164 | ); 165 | } 166 | 167 | /** 168 | * Add several objects. 169 | * 170 | * @param array $objects contains an array of objects to add. If the object contains an objectID 171 | * @param string $objectIDKey 172 | * 173 | * @return mixed 174 | */ 175 | public function addObjects($objects, $objectIDKey = 'objectID') 176 | { 177 | $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); 178 | 179 | $requests = $this->buildBatch('addObject', $objects, true, $objectIDKey); 180 | 181 | return $this->batch($requests, $requestHeaders); 182 | } 183 | 184 | /** 185 | * Get an object from this index. 186 | * 187 | * @param string $objectID the unique identifier of the object to retrieve 188 | * @param string[] $attributesToRetrieve (optional) if set, contains the list of attributes to retrieve 189 | * @param array $requestHeaders 190 | * 191 | * @return mixed 192 | */ 193 | public function getObject($objectID, $attributesToRetrieve = null) 194 | { 195 | $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); 196 | 197 | $id = urlencode($objectID); 198 | if ($attributesToRetrieve === null) { 199 | return $this->client->request( 200 | $this->context, 201 | 'GET', 202 | '/1/indexes/'.$this->urlIndexName.'/'.$id, 203 | null, 204 | null, 205 | $this->context->readHostsArray, 206 | $this->context->connectTimeout, 207 | $this->context->readTimeout, 208 | $requestHeaders 209 | ); 210 | } 211 | 212 | if (is_array($attributesToRetrieve)) { 213 | $attributesToRetrieve = implode(',', $attributesToRetrieve); 214 | } 215 | 216 | return $this->client->request( 217 | $this->context, 218 | 'GET', 219 | '/1/indexes/'.$this->urlIndexName.'/'.$id, 220 | array('attributes' => $attributesToRetrieve), 221 | null, 222 | $this->context->readHostsArray, 223 | $this->context->connectTimeout, 224 | $this->context->readTimeout, 225 | $requestHeaders 226 | ); 227 | } 228 | 229 | /** 230 | * Get several objects from this index. 231 | * 232 | * @param array $objectIDs the array of unique identifier of objects to retrieve 233 | * @param string[] $attributesToRetrieve (optional) if set, contains the list of attributes to retrieve 234 | * @param array $requestHeaders 235 | * 236 | * @return mixed 237 | * 238 | * @throws \Exception 239 | */ 240 | public function getObjects($objectIDs, $attributesToRetrieve = null) 241 | { 242 | $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); 243 | 244 | if ($objectIDs == null) { 245 | throw new \Exception('No list of objectID provided'); 246 | } 247 | 248 | $requests = array(); 249 | foreach ($objectIDs as $object) { 250 | $req = array('indexName' => $this->indexName, 'objectID' => $object); 251 | 252 | if ($attributesToRetrieve) { 253 | if (is_array($attributesToRetrieve)) { 254 | $attributesToRetrieve = implode(',', $attributesToRetrieve); 255 | } 256 | 257 | $req['attributesToRetrieve'] = $attributesToRetrieve; 258 | } 259 | 260 | array_push($requests, $req); 261 | } 262 | 263 | return $this->client->request( 264 | $this->context, 265 | 'POST', 266 | '/1/indexes/*/objects', 267 | array(), 268 | array('requests' => $requests), 269 | $this->context->readHostsArray, 270 | $this->context->connectTimeout, 271 | $this->context->readTimeout, 272 | $requestHeaders 273 | ); 274 | } 275 | 276 | /** 277 | * Update partially an object (only update attributes passed in argument). 278 | * 279 | * @param array $partialObject contains the object attributes to override, the 280 | * object must contains an objectID attribute 281 | * @param bool $createIfNotExists 282 | * 283 | * @return mixed 284 | * 285 | * @throws AlgoliaException 286 | */ 287 | public function partialUpdateObject($partialObject, $createIfNotExists = true) 288 | { 289 | $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); 290 | 291 | $queryString = $createIfNotExists ? '' : '?createIfNotExists=false'; 292 | 293 | return $this->client->request( 294 | $this->context, 295 | 'POST', 296 | '/1/indexes/'.$this->urlIndexName.'/'.urlencode($partialObject['objectID']).'/partial'.$queryString, 297 | array(), 298 | $partialObject, 299 | $this->context->writeHostsArray, 300 | $this->context->connectTimeout, 301 | $this->context->readTimeout, 302 | $requestHeaders 303 | ); 304 | } 305 | 306 | /** 307 | * Partially Override the content of several objects. 308 | * 309 | * @param array $objects contains an array of objects to update (each object must contains a objectID attribute) 310 | * @param string $objectIDKey 311 | * @param bool $createIfNotExists 312 | * 313 | * @return mixed 314 | */ 315 | public function partialUpdateObjects($objects, $objectIDKey = 'objectID', $createIfNotExists = true) 316 | { 317 | $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); 318 | if ($createIfNotExists) { 319 | $requests = $this->buildBatch('partialUpdateObject', $objects, true, $objectIDKey); 320 | } else { 321 | $requests = $this->buildBatch('partialUpdateObjectNoCreate', $objects, true, $objectIDKey); 322 | } 323 | 324 | return $this->batch($requests, $requestHeaders); 325 | } 326 | 327 | /** 328 | * Override the content of object. 329 | * 330 | * @param array $object contains the object to save, the object must contains an objectID attribute 331 | * or attribute specified in $objectIDKey considered as objectID 332 | * @param string $objectIDKey 333 | * 334 | * @return mixed 335 | */ 336 | public function saveObject($object, $objectIDKey = 'objectID') 337 | { 338 | $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); 339 | 340 | return $this->client->request( 341 | $this->context, 342 | 'PUT', 343 | '/1/indexes/'.$this->urlIndexName.'/'.urlencode($object[$objectIDKey]), 344 | array(), 345 | $object, 346 | $this->context->writeHostsArray, 347 | $this->context->connectTimeout, 348 | $this->context->readTimeout, 349 | $requestHeaders 350 | ); 351 | } 352 | 353 | /** 354 | * Override the content of several objects. 355 | * 356 | * @param array $objects contains an array of objects to update (each object must contains a objectID attribute) 357 | * @param string $objectIDKey 358 | * 359 | * @return mixed 360 | */ 361 | public function saveObjects($objects, $objectIDKey = 'objectID') 362 | { 363 | $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); 364 | 365 | $requests = $this->buildBatch('updateObject', $objects, true, $objectIDKey); 366 | 367 | return $this->batch($requests, $requestHeaders); 368 | } 369 | 370 | /** 371 | * Delete an object from the index. 372 | * 373 | * @param int|string $objectID the unique identifier of object to delete 374 | * 375 | * @return mixed 376 | * 377 | * @throws AlgoliaException 378 | * @throws \Exception 379 | */ 380 | public function deleteObject($objectID) 381 | { 382 | $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); 383 | 384 | if ($objectID == null || mb_strlen($objectID) == 0) { 385 | throw new \Exception('objectID is mandatory'); 386 | } 387 | 388 | return $this->client->request( 389 | $this->context, 390 | 'DELETE', 391 | '/1/indexes/'.$this->urlIndexName.'/'.urlencode($objectID), 392 | null, 393 | null, 394 | $this->context->writeHostsArray, 395 | $this->context->connectTimeout, 396 | $this->context->readTimeout, 397 | $requestHeaders 398 | ); 399 | } 400 | 401 | /** 402 | * Delete several objects. 403 | * 404 | * @param array $objects contains an array of objectIDs to delete. If the object contains an objectID 405 | * 406 | * @return mixed 407 | */ 408 | public function deleteObjects($objects) 409 | { 410 | $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); 411 | 412 | $objectIDs = array(); 413 | foreach ($objects as $key => $id) { 414 | $objectIDs[$key] = array('objectID' => $id); 415 | } 416 | $requests = $this->buildBatch('deleteObject', $objectIDs, true); 417 | 418 | return $this->batch($requests, $requestHeaders); 419 | } 420 | 421 | public function deleteBy(array $args) 422 | { 423 | return $this->client->request( 424 | $this->context, 425 | 'POST', 426 | '/1/indexes/'.$this->urlIndexName.'/deleteByQuery', 427 | null, 428 | array('params' => $this->client->buildQuery($args)), 429 | $this->context->writeHostsArray, 430 | $this->context->connectTimeout, 431 | $this->context->readTimeout 432 | ); 433 | } 434 | 435 | /** 436 | * @deprecated use `deleteBy()` instead. 437 | * Delete all objects matching a query. 438 | * 439 | * @param string $query the query string 440 | * @param array $args the optional query parameters 441 | * @param bool $waitLastCall 442 | * /!\ Be safe with "waitLastCall" 443 | * In really rare cases you can have the number of hits smaller than the hitsPerPage 444 | * param if you trigger the timeout of the search, in that case you won't remove all 445 | * the records 446 | * 447 | * @return int the number of delete operations 448 | */ 449 | public function deleteByQuery($query, $args = array(), $waitLastCall = true) 450 | { 451 | $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); 452 | 453 | $args['attributesToRetrieve'] = 'objectID'; 454 | $args['hitsPerPage'] = 1000; 455 | $args['distinct'] = false; 456 | 457 | $deletedCount = 0; 458 | $results = $this->search($query, $args, $requestHeaders); 459 | while ($results['nbHits'] != 0) { 460 | $objectIDs = array(); 461 | foreach ($results['hits'] as $elt) { 462 | array_push($objectIDs, $elt['objectID']); 463 | } 464 | $res = $this->deleteObjects($objectIDs, $requestHeaders); 465 | $deletedCount += count($objectIDs); 466 | if ($results['nbHits'] < $args['hitsPerPage'] && false === $waitLastCall) { 467 | break; 468 | } 469 | $this->waitTask($res['taskID'], 100, $requestHeaders); 470 | $results = $this->search($query, $args, $requestHeaders); 471 | } 472 | 473 | return $deletedCount; 474 | } 475 | 476 | /** 477 | * Search inside the index. 478 | * 479 | * @param string $query the full text query 480 | * @param mixed $args (optional) if set, contains an associative array with query parameters: 481 | * - page: (integer) Pagination parameter used to select the page to retrieve. 482 | * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 483 | * - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. 484 | * Defaults to 20. 485 | * - attributesToRetrieve: a string that contains the list of object attributes you want to 486 | * retrieve (let you minimize the answer size). Attributes are separated with a comma (for 487 | * example "name,address"). You can also use a string array encoding (for example 488 | * ["name","address"]). By default, all attributes are retrieved. You can also use '*' to 489 | * retrieve all values when an attributesToRetrieve setting is specified for your index. 490 | * - attributesToHighlight: a string that contains the list of attributes you want to highlight 491 | * according to the query. Attributes are separated by a comma. You can also use a string array 492 | * encoding (for example ["name","address"]). If an attribute has no match for the query, the raw 493 | * value is returned. By default all indexed text attributes are highlighted. You can use `*` if 494 | * you want to highlight all textual attributes. Numerical attributes are not highlighted. A 495 | * matchLevel is returned for each highlighted attribute and can contain: 496 | * - full: if all the query terms were found in the attribute, 497 | * - partial: if only some of the query terms were found, 498 | * - none: if none of the query terms were found. 499 | * - attributesToSnippet: a string that contains the list of attributes to snippet alongside the 500 | * number of words to return (syntax is `attributeName:nbWords`). Attributes are separated by a 501 | * comma (Example: attributesToSnippet=name:10,content:10). You can also use a string array 502 | * encoding (Example: attributesToSnippet: ["name:10","content:10"]). By default no snippet is 503 | * computed. 504 | * - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in 505 | * this word. Defaults to 3. 506 | * - minWordSizefor2Typos: the minimum number of characters in a query word to accept two typos 507 | * in this word. Defaults to 7. 508 | * - getRankingInfo: if set to 1, the result hits will contain ranking information in 509 | * _rankingInfo attribute. 510 | * - aroundLatLng: search for entries around a given latitude/longitude (specified as two floats 511 | * separated by a comma). For example aroundLatLng=47.316669,5.016670). You can specify the 512 | * maximum distance in meters with the aroundRadius parameter (in meters) and the precision for 513 | * ranking with aroundPrecision 514 | * (for example if you set aroundPrecision=100, two objects that are distant of less than 100m 515 | * will be considered as identical for "geo" ranking parameter). At indexing, you should specify 516 | * geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, 517 | * "lng":2.348800}}) 518 | * - insideBoundingBox: search entries inside a given area defined by the two extreme points of a 519 | * rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng). For example 520 | * insideBoundingBox=47.3165,4.9665,47.3424,5.0201). At indexing, you should specify geoloc of an 521 | * object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) 522 | * - numericFilters: a string that contains the list of numeric filters you want to apply 523 | * separated by a comma. The syntax of one filter is `attributeName` followed by `operand` 524 | * followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`. You can have 525 | * multiple conditions on one attribute like for example numericFilters=price>100,price<1000. You 526 | * can also use a string array encoding (for example numericFilters: ["price>100","price<1000"]). 527 | * - tagFilters: filter the query by a set of tags. You can AND tags by separating them by 528 | * commas. 529 | * To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 530 | * OR tag3). You can also use a string array encoding, for example tagFilters: 531 | * ["tag1",["tag2","tag3"]] means tag1 AND (tag2 OR tag3). At indexing, tags should be added in 532 | * the _tags** attribute of objects (for example {"_tags":["tag1","tag2"]}). 533 | * - facetFilters: filter the query by a list of facets. 534 | * Facets are separated by commas and each facet is encoded as `attributeName:value`. 535 | * For example: `facetFilters=category:Book,author:John%20Doe`. 536 | * You can also use a string array encoding (for example 537 | * `["category:Book","author:John%20Doe"]`). 538 | * - facets: List of object attributes that you want to use for faceting. 539 | * Attributes are separated with a comma (for example `"category,author"` ). 540 | * You can also use a JSON string array encoding (for example ["category","author"]). 541 | * Only attributes that have been added in **attributesForFaceting** index setting can be used in 542 | * this parameter. You can also use `*` to perform faceting on all attributes specified in 543 | * **attributesForFaceting**. 544 | * - queryType: select how the query words are interpreted, it can be one of the following value: 545 | * - prefixAll: all query words are interpreted as prefixes, 546 | * - prefixLast: only the last word is interpreted as a prefix (default behavior), 547 | * - prefixNone: no query word is interpreted as a prefix. This option is not recommended. 548 | * - optionalWords: a string that contains the list of words that should be considered as 549 | * optional when found in the query. The list of words is comma separated. 550 | * - distinct: If set to 1, enable the distinct feature (disabled by default) if the 551 | * attributeForDistinct index setting is set. This feature is similar to the SQL "distinct" 552 | * keyword: when enabled in a query with the distinct=1 parameter, all hits containing a 553 | * duplicate value for the attributeForDistinct attribute are removed from results. For example, 554 | * if the chosen attribute is show_name and several hits have the same value for show_name, then 555 | * only the best one is kept and others are removed. 556 | * @param array $requestHeaders 557 | * 558 | * @return mixed 559 | * @throws AlgoliaException 560 | */ 561 | public function search($query, $args = null) 562 | { 563 | $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); 564 | 565 | if ($args === null) { 566 | $args = array(); 567 | } 568 | $args['query'] = $query; 569 | 570 | if (isset($args['disjunctiveFacets'])) { 571 | return $this->searchWithDisjunctiveFaceting($query, $args); 572 | } 573 | 574 | return $this->client->request( 575 | $this->context, 576 | 'POST', 577 | '/1/indexes/'.$this->urlIndexName.'/query', 578 | array(), 579 | array('params' => $this->client->buildQuery($args)), 580 | $this->context->readHostsArray, 581 | $this->context->connectTimeout, 582 | $this->context->searchTimeout, 583 | $requestHeaders 584 | ); 585 | } 586 | 587 | /** 588 | * @param $query 589 | * @param $args 590 | * @return mixed 591 | * @throws AlgoliaException 592 | */ 593 | private function searchWithDisjunctiveFaceting($query, $args) 594 | { 595 | $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); 596 | 597 | if (! is_array($args['disjunctiveFacets']) || count($args['disjunctiveFacets']) <= 0) { 598 | throw new \InvalidArgumentException('disjunctiveFacets needs to be an non empty array'); 599 | } 600 | 601 | if (isset($args['filters'])) { 602 | throw new \InvalidArgumentException('You can not use disjunctive faceting and the filters parameter'); 603 | } 604 | 605 | /** 606 | * Prepare queries 607 | */ 608 | // Get the list of disjunctive queries to do: 1 per disjunctive facet 609 | $disjunctiveQueries = $this->getDisjunctiveQueries($args); 610 | 611 | // Format disjunctive queries for multipleQueries call 612 | foreach ($disjunctiveQueries as &$disjunctiveQuery) { 613 | $disjunctiveQuery['indexName'] = $this->indexName; 614 | $disjunctiveQuery['query'] = $query; 615 | unset($disjunctiveQuery['disjunctiveFacets']); 616 | } 617 | 618 | // Merge facets and disjunctiveFacets for the hits query 619 | $facets = isset($args['facets']) ? $args['facets'] : array(); 620 | $facets = array_merge($facets, $args['disjunctiveFacets']); 621 | unset($args['disjunctiveFacets']); 622 | 623 | // format the hits query for multipleQueries call 624 | $args['query'] = $query; 625 | $args['indexName'] = $this->indexName; 626 | $args['facets'] = $facets; 627 | 628 | // Put the hit query first 629 | array_unshift($disjunctiveQueries, $args); 630 | 631 | /** 632 | * Do all queries in one call 633 | */ 634 | $results = $this->client->multipleQueries( 635 | array_values($disjunctiveQueries), 636 | 'indexName', 637 | 'none', 638 | $requestHeaders 639 | ); 640 | $results = $results['results']; 641 | 642 | /** 643 | * Merge facets from disjunctive queries with facets from the hits query 644 | */ 645 | 646 | // The first query is the hits query that the one we'll return to the user 647 | $queryResults = array_shift($results); 648 | 649 | // To be able to add facets from disjunctive query we create 'facets' key in case we only have disjunctive facets 650 | if (false === isset($queryResults['facets'])) { 651 | $queryResults['facets'] = array(); 652 | } 653 | 654 | foreach ($results as $disjunctiveResults) { 655 | if (isset($disjunctiveResults['facets'])) { 656 | foreach ($disjunctiveResults['facets'] as $facetName => $facetValues) { 657 | $queryResults['facets'][$facetName] = $facetValues; 658 | } 659 | } 660 | } 661 | 662 | return $queryResults; 663 | } 664 | 665 | /** 666 | * @param $queryParams 667 | * @return array 668 | */ 669 | private function getDisjunctiveQueries($queryParams) 670 | { 671 | $queriesParams = array(); 672 | 673 | foreach ($queryParams['disjunctiveFacets'] as $facetName) { 674 | $params = $queryParams; 675 | $params['facets'] = array($facetName); 676 | $facetFilters = isset($params['facetFilters']) ? $params['facetFilters']: array(); 677 | $numericFilters = isset($params['numericFilters']) ? $params['numericFilters']: array(); 678 | 679 | $additionalParams = array( 680 | 'hitsPerPage' => 1, 681 | 'page' => 0, 682 | 'attributesToRetrieve' => array(), 683 | 'attributesToHighlight' => array(), 684 | 'attributesToSnippet' => array(), 685 | 'analytics' => false 686 | ); 687 | 688 | $additionalParams['facetFilters'] = $this->getAlgoliaFiltersArrayWithoutCurrentRefinement($facetFilters, $facetName . ':'); 689 | $additionalParams['numericFilters'] = $this->getAlgoliaFiltersArrayWithoutCurrentRefinement($numericFilters, $facetName); 690 | 691 | $queriesParams[$facetName] = array_merge($params, $additionalParams); 692 | } 693 | 694 | return $queriesParams; 695 | } 696 | 697 | /** 698 | * @param $filters 699 | * @param $needle 700 | * @return array 701 | */ 702 | private function getAlgoliaFiltersArrayWithoutCurrentRefinement($filters, $needle) 703 | { 704 | // iterate on each filters which can be string or array and filter out every refinement matching the needle 705 | for ($i = 0; $i < count($filters); $i++) { 706 | if (is_array($filters[$i])) { 707 | foreach ($filters[$i] as $filter) { 708 | if (mb_substr($filter, 0, mb_strlen($needle)) === $needle) { 709 | unset($filters[$i]); 710 | $filters = array_values($filters); 711 | $i--; 712 | break; 713 | } 714 | } 715 | } else { 716 | if (mb_substr($filters[$i], 0, mb_strlen($needle)) === $needle) { 717 | unset($filters[$i]); 718 | $filters = array_values($filters); 719 | $i--; 720 | } 721 | } 722 | } 723 | 724 | return $filters; 725 | } 726 | 727 | /** 728 | * Perform a search inside facets. 729 | * 730 | * @param $facetName 731 | * @param $facetQuery 732 | * @param array $query 733 | * @param array $requestHeaders 734 | * 735 | * @return mixed 736 | */ 737 | public function searchForFacetValues($facetName, $facetQuery, $query = array()) 738 | { 739 | $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); 740 | 741 | $query['facetQuery'] = $facetQuery; 742 | 743 | return $this->client->request( 744 | $this->context, 745 | 'POST', 746 | '/1/indexes/'.$this->urlIndexName.'/facets/'.$facetName.'/query', 747 | array(), 748 | array('params' => $this->client->buildQuery($query)), 749 | $this->context->readHostsArray, 750 | $this->context->connectTimeout, 751 | $this->context->searchTimeout, 752 | $requestHeaders 753 | ); 754 | } 755 | 756 | /** 757 | * Perform a search with disjunctive facets generating as many queries as number of disjunctive facets. 758 | * 759 | * @param string $query the query 760 | * @param array $disjunctive_facets the array of disjunctive facets 761 | * @param array $params a hash representing the regular query parameters 762 | * @param array $refinements a hash ("string" -> ["array", "of", "refined", "values"]) representing the current refinements 763 | * ex: { "my_facet1" => ["my_value1", ["my_value2"], "my_disjunctive_facet1" => ["my_value1", "my_value2"] } 764 | * 765 | * @return mixed 766 | * 767 | * @throws AlgoliaException 768 | * @throws \Exception 769 | * @deprecated you should use $index->search($query, ['disjunctiveFacets' => $disjunctive_facets]]); instead 770 | */ 771 | public function searchDisjunctiveFaceting($query, $disjunctive_facets, $params = array(), $refinements = array()) 772 | { 773 | if (gettype($disjunctive_facets) != 'string' && gettype($disjunctive_facets) != 'array') { 774 | throw new AlgoliaException('Argument "disjunctive_facets" must be a String or an Array'); 775 | } 776 | 777 | if (gettype($refinements) != 'array') { 778 | throw new AlgoliaException('Argument "refinements" must be a Hash of Arrays'); 779 | } 780 | 781 | if (gettype($disjunctive_facets) == 'string') { 782 | $disjunctive_facets = explode(',', $disjunctive_facets); 783 | } 784 | 785 | $disjunctive_refinements = array(); 786 | foreach ($refinements as $key => $value) { 787 | if (in_array($key, $disjunctive_facets)) { 788 | $disjunctive_refinements[$key] = $value; 789 | } 790 | } 791 | $queries = array(); 792 | $filters = array(); 793 | 794 | foreach ($refinements as $key => $value) { 795 | $r = array_map( 796 | function ($val) use ($key) { 797 | return $key.':'.$val; 798 | }, 799 | $value 800 | ); 801 | 802 | if (in_array($key, $disjunctive_refinements)) { 803 | $filter = array_merge($filters, $r); 804 | } else { 805 | array_push($filters, $r); 806 | } 807 | } 808 | $params['indexName'] = $this->indexName; 809 | $params['query'] = $query; 810 | $params['facetFilters'] = $filters; 811 | array_push($queries, $params); 812 | foreach ($disjunctive_facets as $disjunctive_facet) { 813 | $filters = array(); 814 | foreach ($refinements as $key => $value) { 815 | if ($key != $disjunctive_facet) { 816 | $r = array_map( 817 | function ($val) use ($key) { 818 | return $key.':'.$val; 819 | }, 820 | $value 821 | ); 822 | 823 | if (in_array($key, $disjunctive_refinements)) { 824 | $filter = array_merge($filters, $r); 825 | } else { 826 | array_push($filters, $r); 827 | } 828 | } 829 | } 830 | $params['indexName'] = $this->indexName; 831 | $params['query'] = $query; 832 | $params['facetFilters'] = $filters; 833 | $params['page'] = 0; 834 | $params['hitsPerPage'] = 0; 835 | $params['attributesToRetrieve'] = array(); 836 | $params['attributesToHighlight'] = array(); 837 | $params['attributesToSnippet'] = array(); 838 | $params['facets'] = $disjunctive_facet; 839 | $params['analytics'] = false; 840 | array_push($queries, $params); 841 | } 842 | $answers = $this->client->multipleQueries($queries); 843 | 844 | $aggregated_answer = $answers['results'][0]; 845 | $aggregated_answer['disjunctiveFacets'] = array(); 846 | for ($i = 1; $i < count($answers['results']); $i++) { 847 | foreach ($answers['results'][$i]['facets'] as $key => $facet) { 848 | $aggregated_answer['disjunctiveFacets'][$key] = $facet; 849 | if (!in_array($key, $disjunctive_refinements)) { 850 | continue; 851 | } 852 | foreach ($disjunctive_refinements[$key] as $r) { 853 | if (is_null($aggregated_answer['disjunctiveFacets'][$key][$r])) { 854 | $aggregated_answer['disjunctiveFacets'][$key][$r] = 0; 855 | } 856 | } 857 | } 858 | } 859 | 860 | return $aggregated_answer; 861 | } 862 | 863 | /** 864 | * Browse all index content. 865 | * 866 | * @param int $page Pagination parameter used to select the page to retrieve. 867 | * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 868 | * @param int $hitsPerPage : Pagination parameter used to select the number of hits per page. Defaults to 1000. 869 | * 870 | * @return mixed 871 | * 872 | * @throws AlgoliaException 873 | */ 874 | private function doBcBrowse($page = 0, $hitsPerPage = 1000) 875 | { 876 | return $this->client->request( 877 | $this->context, 878 | 'GET', 879 | '/1/indexes/'.$this->urlIndexName.'/browse', 880 | array('page' => $page, 'hitsPerPage' => $hitsPerPage), 881 | null, 882 | $this->context->readHostsArray, 883 | $this->context->connectTimeout, 884 | $this->context->readTimeout 885 | ); 886 | } 887 | 888 | /** 889 | * Wait the publication of a task on the server. 890 | * All server task are asynchronous and you can check with this method that the task is published. 891 | * 892 | * @param string $taskID the id of the task returned by server 893 | * @param int $timeBeforeRetry the time in milliseconds before retry (default = 100ms) 894 | * 895 | * @return mixed 896 | */ 897 | public function waitTask($taskID, $timeBeforeRetry = 100) 898 | { 899 | $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); 900 | 901 | while (true) { 902 | $res = $this->getTaskStatus($taskID, $requestHeaders); 903 | if ($res['status'] === 'published') { 904 | return $res; 905 | } 906 | usleep($timeBeforeRetry * 1000); 907 | } 908 | } 909 | 910 | /** 911 | * get the status of a task on the server. 912 | * All server task are asynchronous and you can check with this method that the task is published or not. 913 | * 914 | * @param string $taskID the id of the task returned by server 915 | * 916 | * @return mixed 917 | */ 918 | public function getTaskStatus($taskID) 919 | { 920 | $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); 921 | 922 | return $this->client->request( 923 | $this->context, 924 | 'GET', 925 | '/1/indexes/'.$this->urlIndexName.'/task/'.$taskID, 926 | null, 927 | null, 928 | $this->context->readHostsArray, 929 | $this->context->connectTimeout, 930 | $this->context->readTimeout, 931 | $requestHeaders 932 | ); 933 | } 934 | 935 | /** 936 | * Get settings of this index. 937 | * 938 | * @return mixed 939 | * 940 | * @throws AlgoliaException 941 | */ 942 | public function getSettings() 943 | { 944 | $requestHeaders = func_num_args() === 1 && is_array(func_get_arg(0)) ? func_get_arg(0) : array(); 945 | 946 | return $this->client->request( 947 | $this->context, 948 | 'GET', 949 | '/1/indexes/'.$this->urlIndexName.'/settings?getVersion=2', 950 | null, 951 | null, 952 | $this->context->readHostsArray, 953 | $this->context->connectTimeout, 954 | $this->context->readTimeout, 955 | $requestHeaders 956 | ); 957 | } 958 | 959 | /** 960 | * This function deletes the index content. Settings and index specific API keys are kept untouched. 961 | * 962 | * @return mixed 963 | * 964 | * @throws AlgoliaException 965 | */ 966 | public function clearIndex() 967 | { 968 | $requestHeaders = func_num_args() === 1 && is_array(func_get_arg(0)) ? func_get_arg(0) : array(); 969 | 970 | return $this->client->request( 971 | $this->context, 972 | 'POST', 973 | '/1/indexes/'.$this->urlIndexName.'/clear', 974 | null, 975 | null, 976 | $this->context->writeHostsArray, 977 | $this->context->connectTimeout, 978 | $this->context->readTimeout, 979 | $requestHeaders 980 | ); 981 | } 982 | 983 | /** 984 | * Set settings for this index. 985 | * 986 | * @param mixed $settings the settings object that can contains : 987 | * - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 988 | * 3). 989 | * - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default 990 | * = 7). 991 | * - hitsPerPage: (integer) the number of hits per page (default = 10). 992 | * - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects. 993 | * If set to null, all attributes are retrieved. 994 | * - attributesToHighlight: (array of strings) default list of attributes to highlight. 995 | * If set to null, all indexed attributes are highlighted. 996 | * - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the 997 | * number of words to return (syntax is attributeName:nbWords). By default no snippet is computed. 998 | * If set to null, no snippet is computed. 999 | * - searchableAttributes (formerly named attributesToIndex): (array of strings) the list of fields you want to index. 1000 | * If set to null, all textual and numerical attributes of your objects are indexed, but you 1001 | * should update it to get optimal results. This parameter has two important uses: 1002 | * - Limit the attributes to index: For example if you store a binary image in base64, you want to 1003 | * store it and be able to retrieve it but you don't want to search in the base64 string. 1004 | * - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in 1005 | * attributes at the beginning of the list will be considered more important than matches in 1006 | * attributes further down the list. In one attribute, matching text at the beginning of the 1007 | * attribute will be considered more important than text after, you can disable this behavior if 1008 | * you add your attribute inside `unordered(AttributeName)`, for example searchableAttributes: 1009 | * ["title", "unordered(text)"]. 1010 | * - attributesForFaceting: (array of strings) The list of fields you want to use for faceting. 1011 | * All strings in the attribute selected for faceting are extracted and added as a facet. If set 1012 | * to null, no attribute is used for faceting. 1013 | * - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature 1014 | * is similar to the SQL "distinct" keyword: when enabled in query with the distinct=1 parameter, 1015 | * all hits containing a duplicate value for this attribute are removed from results. For example, 1016 | * if the chosen attribute is show_name and several hits have the same value for show_name, then 1017 | * only the best one is kept and others are removed. 1018 | * - ranking: (array of strings) controls the way results are sorted. 1019 | * We have six available criteria: 1020 | * - typo: sort according to number of typos, 1021 | * - geo: sort according to decreasing distance when performing a geo-location based search, 1022 | * - proximity: sort according to the proximity of query words in hits, 1023 | * - attribute: sort according to the order of attributes defined by searchableAttributes, 1024 | * - exact: 1025 | * - if the user query contains one word: sort objects having an attribute that is exactly the 1026 | * query word before others. For example if you search for the "V" TV show, you want to find it 1027 | * with the "V" query and avoid to have all popular TV show starting by the v letter before it. 1028 | * - if the user query contains multiple words: sort according to the number of words that matched 1029 | * exactly (and not as a prefix). 1030 | * - custom: sort according to a user defined formula set in **customRanking** attribute. 1031 | * The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"] 1032 | * - customRanking: (array of strings) lets you specify part of the ranking. 1033 | * The syntax of this condition is an array of strings containing attributes prefixed by asc 1034 | * (ascending order) or desc (descending order) operator. For example `"customRanking" => 1035 | * ["desc(population)", "asc(name)"]` 1036 | * - queryType: Select how the query words are interpreted, it can be one of the following value: 1037 | * - prefixAll: all query words are interpreted as prefixes, 1038 | * - prefixLast: only the last word is interpreted as a prefix (default behavior), 1039 | * - prefixNone: no query word is interpreted as a prefix. This option is not recommended. 1040 | * - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in 1041 | * the query result (default to ""). 1042 | * - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in 1043 | * the query result (default to ""). 1044 | * - optionalWords: (array of strings) Specify a list of words that should be considered as 1045 | * optional when found in the query. 1046 | * @param bool $forwardToReplicas 1047 | * 1048 | * @return mixed 1049 | * 1050 | * @throws AlgoliaException 1051 | */ 1052 | public function setSettings($settings, $forwardToReplicas = false) 1053 | { 1054 | $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); 1055 | 1056 | $url = '/1/indexes/'.$this->urlIndexName.'/settings'; 1057 | 1058 | if ($forwardToReplicas) { 1059 | $url = $url.'?forwardToReplicas=true'; 1060 | } 1061 | 1062 | return $this->client->request( 1063 | $this->context, 1064 | 'PUT', 1065 | $url, 1066 | array(), 1067 | $settings, 1068 | $this->context->writeHostsArray, 1069 | $this->context->connectTimeout, 1070 | $this->context->readTimeout, 1071 | $requestHeaders 1072 | ); 1073 | } 1074 | 1075 | /** 1076 | * List all existing API keys associated to this index with their associated ACLs. 1077 | * 1078 | * @return mixed 1079 | * 1080 | * @throws AlgoliaException 1081 | */ 1082 | public function listApiKeys() 1083 | { 1084 | $requestHeaders = func_num_args() === 1 && is_array(func_get_arg(0)) ? func_get_arg(0) : array(); 1085 | 1086 | return $this->client->request( 1087 | $this->context, 1088 | 'GET', 1089 | '/1/indexes/'.$this->urlIndexName.'/keys', 1090 | null, 1091 | null, 1092 | $this->context->readHostsArray, 1093 | $this->context->connectTimeout, 1094 | $this->context->readTimeout, 1095 | $requestHeaders 1096 | ); 1097 | } 1098 | 1099 | /** 1100 | * @deprecated use listApiKeys instead 1101 | * @return mixed 1102 | */ 1103 | public function listUserKeys() 1104 | { 1105 | return $this->listApiKeys(); 1106 | } 1107 | 1108 | /** 1109 | * @deprecated use getApiKey in 1110 | * @param $key 1111 | * @return mixed 1112 | */ 1113 | public function getUserKeyACL($key) 1114 | { 1115 | return $this->getApiKey($key); 1116 | } 1117 | 1118 | /** 1119 | * Get ACL of a API key associated to this index. 1120 | * 1121 | * @param string $key 1122 | * 1123 | * @return mixed 1124 | * 1125 | * @throws AlgoliaException 1126 | */ 1127 | public function getApiKey($key) 1128 | { 1129 | $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); 1130 | 1131 | return $this->client->request( 1132 | $this->context, 1133 | 'GET', 1134 | '/1/indexes/'.$this->urlIndexName.'/keys/'.$key, 1135 | null, 1136 | null, 1137 | $this->context->readHostsArray, 1138 | $this->context->connectTimeout, 1139 | $this->context->readTimeout, 1140 | $requestHeaders 1141 | ); 1142 | } 1143 | 1144 | 1145 | /** 1146 | * Delete an existing API key associated to this index. 1147 | * 1148 | * @param string $key 1149 | * 1150 | * @return mixed 1151 | * 1152 | * @throws AlgoliaException 1153 | */ 1154 | public function deleteApiKey($key) 1155 | { 1156 | $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); 1157 | 1158 | return $this->client->request( 1159 | $this->context, 1160 | 'DELETE', 1161 | '/1/indexes/'.$this->urlIndexName.'/keys/'.$key, 1162 | null, 1163 | null, 1164 | $this->context->writeHostsArray, 1165 | $this->context->connectTimeout, 1166 | $this->context->readTimeout, 1167 | $requestHeaders 1168 | ); 1169 | } 1170 | 1171 | /** 1172 | * @param $key 1173 | * @return mixed 1174 | * @deprecated use deleteApiKey instead 1175 | */ 1176 | public function deleteUserKey($key) 1177 | { 1178 | return $this->deleteApiKey($key); 1179 | } 1180 | 1181 | /** 1182 | * Create a new API key associated to this index. 1183 | * 1184 | * @param array $obj can be two different parameters: 1185 | * The list of parameters for this key. Defined by a array that 1186 | * can contains the following values: 1187 | * - acl: array of string 1188 | * - indices: array of string 1189 | * - validity: int 1190 | * - referers: array of string 1191 | * - description: string 1192 | * - maxHitsPerQuery: integer 1193 | * - queryParameters: string 1194 | * - maxQueriesPerIPPerHour: integer 1195 | * Or the list of ACL for this key. Defined by an array of NSString that 1196 | * can contains the following values: 1197 | * - search: allow to search (https and http) 1198 | * - addObject: allows to add/update an object in the index (https only) 1199 | * - deleteObject : allows to delete an existing object (https only) 1200 | * - deleteIndex : allows to delete index content (https only) 1201 | * - settings : allows to get index settings (https only) 1202 | * - editSettings : allows to change index settings (https only) 1203 | * @param int $validity the number of seconds after which the key will be automatically removed (0 means 1204 | * no time limit for this key) 1205 | * @param int $maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. 1206 | * Defaults to 0 (no rate limit). 1207 | * @param int $maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. 1208 | * Defaults to 0 (unlimited) 1209 | * 1210 | * @return mixed 1211 | * 1212 | * @throws AlgoliaException 1213 | */ 1214 | public function addApiKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) 1215 | { 1216 | $requestHeaders = func_num_args() === 5 && is_array(func_get_arg(4)) ? func_get_arg(4) : array(); 1217 | 1218 | if ($obj !== array_values($obj)) { 1219 | // if $obj doesn't have required entries, we add the default values 1220 | $params = $obj; 1221 | if ($validity != 0) { 1222 | $params['validity'] = $validity; 1223 | } 1224 | if ($maxQueriesPerIPPerHour != 0) { 1225 | $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour; 1226 | } 1227 | if ($maxHitsPerQuery != 0) { 1228 | $params['maxHitsPerQuery'] = $maxHitsPerQuery; 1229 | } 1230 | } else { 1231 | $params = array( 1232 | 'acl' => $obj, 1233 | 'validity' => $validity, 1234 | 'maxQueriesPerIPPerHour' => $maxQueriesPerIPPerHour, 1235 | 'maxHitsPerQuery' => $maxHitsPerQuery, 1236 | ); 1237 | } 1238 | 1239 | return $this->client->request( 1240 | $this->context, 1241 | 'POST', 1242 | '/1/indexes/'.$this->urlIndexName.'/keys', 1243 | array(), 1244 | $params, 1245 | $this->context->writeHostsArray, 1246 | $this->context->connectTimeout, 1247 | $this->context->readTimeout, 1248 | $requestHeaders 1249 | ); 1250 | } 1251 | 1252 | /** 1253 | * @param $obj 1254 | * @param int $validity 1255 | * @param int $maxQueriesPerIPPerHour 1256 | * @param int $maxHitsPerQuery 1257 | * @return mixed 1258 | * @deprecated use addApiKey instead 1259 | */ 1260 | public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) 1261 | { 1262 | return $this->addApiKey($obj, $validity, $maxQueriesPerIPPerHour, $maxHitsPerQuery); 1263 | } 1264 | 1265 | 1266 | /** 1267 | * Update an API key associated to this index. 1268 | * 1269 | * @param string $key 1270 | * @param array $obj can be two different parameters: 1271 | * The list of parameters for this key. Defined by a array that 1272 | * can contains the following values: 1273 | * - acl: array of string 1274 | * - indices: array of string 1275 | * - validity: int 1276 | * - referers: array of string 1277 | * - description: string 1278 | * - maxHitsPerQuery: integer 1279 | * - queryParameters: string 1280 | * - maxQueriesPerIPPerHour: integer 1281 | * Or the list of ACL for this key. Defined by an array of NSString that 1282 | * can contains the following values: 1283 | * - search: allow to search (https and http) 1284 | * - addObject: allows to add/update an object in the index (https only) 1285 | * - deleteObject : allows to delete an existing object (https only) 1286 | * - deleteIndex : allows to delete index content (https only) 1287 | * - settings : allows to get index settings (https only) 1288 | * - editSettings : allows to change index settings (https only) 1289 | * @param int $validity the number of seconds after which the key will be automatically removed (0 means 1290 | * no time limit for this key) 1291 | * @param int $maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. 1292 | * Defaults to 0 (no rate limit). 1293 | * @param int $maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. 1294 | * Defaults to 0 (unlimited) 1295 | * 1296 | * @return mixed 1297 | * 1298 | * @throws AlgoliaException 1299 | */ 1300 | public function updateApiKey($key, $obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) 1301 | { 1302 | $requestHeaders = func_num_args() === 6 && is_array(func_get_arg(5)) ? func_get_arg(5) : array(); 1303 | 1304 | // is dict of value 1305 | if ($obj !== array_values($obj)) { 1306 | $params = $obj; 1307 | $params['validity'] = $validity; 1308 | $params['maxQueriesPerIPPerHour'] = $maxQueriesPerIPPerHour; 1309 | $params['maxHitsPerQuery'] = $maxHitsPerQuery; 1310 | } else { 1311 | $params = array( 1312 | 'acl' => $obj, 1313 | 'validity' => $validity, 1314 | 'maxQueriesPerIPPerHour' => $maxQueriesPerIPPerHour, 1315 | 'maxHitsPerQuery' => $maxHitsPerQuery, 1316 | ); 1317 | } 1318 | 1319 | return $this->client->request( 1320 | $this->context, 1321 | 'PUT', 1322 | '/1/indexes/'.$this->urlIndexName.'/keys/'.$key, 1323 | array(), 1324 | $params, 1325 | $this->context->writeHostsArray, 1326 | $this->context->connectTimeout, 1327 | $this->context->readTimeout, 1328 | $requestHeaders 1329 | ); 1330 | } 1331 | 1332 | /** 1333 | * @param $key 1334 | * @param $obj 1335 | * @param int $validity 1336 | * @param int $maxQueriesPerIPPerHour 1337 | * @param int $maxHitsPerQuery 1338 | * @return mixed 1339 | * @deprecated use updateApiKey instead 1340 | */ 1341 | public function updateUserKey($key, $obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) 1342 | { 1343 | return $this->updateApiKey($key, $obj, $validity, $maxQueriesPerIPPerHour, $maxHitsPerQuery); 1344 | } 1345 | 1346 | /** 1347 | * Send a batch request. 1348 | * 1349 | * @param array $requests an associative array defining the batch request body 1350 | * @param array $requestHeaders pass custom header only for this request 1351 | * 1352 | * @return mixed 1353 | */ 1354 | public function batch($requests) 1355 | { 1356 | $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); 1357 | 1358 | return $this->client->request( 1359 | $this->context, 1360 | 'POST', 1361 | '/1/indexes/'.$this->urlIndexName.'/batch', 1362 | array(), 1363 | $requests, 1364 | $this->context->writeHostsArray, 1365 | $this->context->connectTimeout, 1366 | $this->context->readTimeout, 1367 | $requestHeaders 1368 | ); 1369 | } 1370 | 1371 | /** 1372 | * Build a batch request. 1373 | * 1374 | * @param string $action the batch action 1375 | * @param array $objects the array of objects 1376 | * @param string $withObjectID set an 'objectID' attribute 1377 | * @param string $objectIDKey the objectIDKey 1378 | * 1379 | * @return array 1380 | */ 1381 | private function buildBatch($action, $objects, $withObjectID, $objectIDKey = 'objectID') 1382 | { 1383 | $requests = array(); 1384 | foreach ($objects as $obj) { 1385 | $req = array('action' => $action, 'body' => $obj); 1386 | if ($withObjectID && array_key_exists($objectIDKey, $obj)) { 1387 | $req['objectID'] = (string) $obj[$objectIDKey]; 1388 | } 1389 | array_push($requests, $req); 1390 | } 1391 | 1392 | return array('requests' => $requests); 1393 | } 1394 | 1395 | /** 1396 | * @param string $query 1397 | * @param array|null $params 1398 | * 1399 | * @return IndexBrowser 1400 | */ 1401 | private function doBrowse($query, $params = null) 1402 | { 1403 | return new IndexBrowser($this, $query, $params); 1404 | } 1405 | 1406 | /** 1407 | * @param string $query 1408 | * @param array|null $params 1409 | * @param $cursor 1410 | * @param array $requestHeaders 1411 | * 1412 | * @return mixed 1413 | */ 1414 | public function browseFrom($query, $params = null, $cursor = null) 1415 | { 1416 | $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); 1417 | 1418 | if ($params === null) { 1419 | $params = array(); 1420 | } 1421 | foreach ($params as $key => $value) { 1422 | if (gettype($value) == 'array') { 1423 | $params[$key] = Json::encode($value); 1424 | } 1425 | } 1426 | if ($query != null) { 1427 | $params['query'] = $query; 1428 | } 1429 | if ($cursor != null) { 1430 | $params['cursor'] = $cursor; 1431 | } 1432 | 1433 | return $this->client->request( 1434 | $this->context, 1435 | 'GET', 1436 | '/1/indexes/'.$this->urlIndexName.'/browse', 1437 | $params, 1438 | null, 1439 | $this->context->readHostsArray, 1440 | $this->context->connectTimeout, 1441 | $this->context->readTimeout, 1442 | $requestHeaders 1443 | ); 1444 | } 1445 | 1446 | /** 1447 | * @param $query 1448 | * @param $synonymType 1449 | * @param null $page 1450 | * @param null $hitsPerPage 1451 | * 1452 | * @return mixed 1453 | * 1454 | * @throws AlgoliaException 1455 | */ 1456 | public function searchSynonyms($query, array $synonymType = array(), $page = null, $hitsPerPage = null) 1457 | { 1458 | $requestHeaders = func_num_args() === 5 && is_array(func_get_arg(4)) ? func_get_arg(4) : array(); 1459 | 1460 | $params = array(); 1461 | 1462 | if ($query !== null) { 1463 | $params['query'] = $query; 1464 | } 1465 | 1466 | if (count($synonymType) > 0) { 1467 | $types = array(); 1468 | 1469 | foreach ($synonymType as $type) { 1470 | if (is_integer($type)) { 1471 | $types[] = SynonymType::getSynonymsTypeString($type); 1472 | } else { 1473 | $types[] = $type; 1474 | } 1475 | } 1476 | $params['type'] = implode(',', $types); 1477 | } 1478 | 1479 | if ($page !== null) { 1480 | $params['page'] = $page; 1481 | } 1482 | 1483 | if ($hitsPerPage !== null) { 1484 | $params['hitsPerPage'] = $hitsPerPage; 1485 | } 1486 | 1487 | return $this->client->request( 1488 | $this->context, 1489 | 'POST', 1490 | '/1/indexes/'.$this->urlIndexName.'/synonyms/search', 1491 | null, 1492 | $params, 1493 | $this->context->readHostsArray, 1494 | $this->context->connectTimeout, 1495 | $this->context->readTimeout, 1496 | $requestHeaders 1497 | ); 1498 | } 1499 | 1500 | /** 1501 | * @param $objectID 1502 | * 1503 | * @return mixed 1504 | * 1505 | * @throws AlgoliaException 1506 | */ 1507 | public function getSynonym($objectID) 1508 | { 1509 | $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); 1510 | 1511 | return $this->client->request( 1512 | $this->context, 1513 | 'GET', 1514 | '/1/indexes/'.$this->urlIndexName.'/synonyms/'.urlencode($objectID), 1515 | null, 1516 | null, 1517 | $this->context->readHostsArray, 1518 | $this->context->connectTimeout, 1519 | $this->context->readTimeout, 1520 | $requestHeaders 1521 | ); 1522 | } 1523 | 1524 | /** 1525 | * @param $objectID 1526 | * @param $forwardToReplicas 1527 | * 1528 | * @return mixed 1529 | * 1530 | * @throws AlgoliaException 1531 | */ 1532 | public function deleteSynonym($objectID, $forwardToReplicas = false) 1533 | { 1534 | $requestHeaders = func_num_args() === 3 && is_array(func_get_arg(2)) ? func_get_arg(2) : array(); 1535 | 1536 | return $this->client->request( 1537 | $this->context, 1538 | 'DELETE', 1539 | '/1/indexes/'.$this->urlIndexName.'/synonyms/'.urlencode($objectID).'?forwardToReplicas='.($forwardToReplicas ? 'true' : 'false'), 1540 | null, 1541 | null, 1542 | $this->context->writeHostsArray, 1543 | $this->context->connectTimeout, 1544 | $this->context->readTimeout, 1545 | $requestHeaders 1546 | ); 1547 | } 1548 | 1549 | /** 1550 | * @param bool $forwardToReplicas 1551 | * 1552 | * @return mixed 1553 | * 1554 | * @throws AlgoliaException 1555 | */ 1556 | public function clearSynonyms($forwardToReplicas = false) 1557 | { 1558 | $requestHeaders = func_num_args() === 2 && is_array(func_get_arg(1)) ? func_get_arg(1) : array(); 1559 | 1560 | return $this->client->request( 1561 | $this->context, 1562 | 'POST', 1563 | '/1/indexes/'.$this->urlIndexName.'/synonyms/clear?forwardToReplicas='.($forwardToReplicas ? 'true' : 'false'), 1564 | null, 1565 | null, 1566 | $this->context->writeHostsArray, 1567 | $this->context->connectTimeout, 1568 | $this->context->readTimeout, 1569 | $requestHeaders 1570 | ); 1571 | } 1572 | 1573 | /** 1574 | * @param $objects 1575 | * @param bool $forwardToReplicas 1576 | * @param bool $replaceExistingSynonyms 1577 | * 1578 | * @return mixed 1579 | * 1580 | * @throws AlgoliaException 1581 | */ 1582 | public function batchSynonyms($objects, $forwardToReplicas = false, $replaceExistingSynonyms = false) 1583 | { 1584 | $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); 1585 | 1586 | return $this->client->request( 1587 | $this->context, 1588 | 'POST', 1589 | '/1/indexes/'.$this->urlIndexName.'/synonyms/batch?replaceExistingSynonyms='.($replaceExistingSynonyms ? 'true' : 'false') 1590 | .'&forwardToReplicas='.($forwardToReplicas ? 'true' : 'false'), 1591 | null, 1592 | $objects, 1593 | $this->context->writeHostsArray, 1594 | $this->context->connectTimeout, 1595 | $this->context->readTimeout, 1596 | $requestHeaders 1597 | ); 1598 | } 1599 | 1600 | /** 1601 | * @param $objectID 1602 | * @param $content 1603 | * @param bool $forwardToReplicas 1604 | * 1605 | * @return mixed 1606 | * 1607 | * @throws AlgoliaException 1608 | */ 1609 | public function saveSynonym($objectID, $content, $forwardToReplicas = false) 1610 | { 1611 | $requestHeaders = func_num_args() === 4 && is_array(func_get_arg(3)) ? func_get_arg(3) : array(); 1612 | 1613 | return $this->client->request( 1614 | $this->context, 1615 | 'PUT', 1616 | '/1/indexes/'.$this->urlIndexName.'/synonyms/'.urlencode($objectID).'?forwardToReplicas='.($forwardToReplicas ? 'true' : 'false'), 1617 | null, 1618 | $content, 1619 | $this->context->writeHostsArray, 1620 | $this->context->connectTimeout, 1621 | $this->context->readTimeout, 1622 | $requestHeaders 1623 | ); 1624 | } 1625 | 1626 | /** 1627 | * @param int $batchSize 1628 | * 1629 | * @return SynonymIterator 1630 | */ 1631 | public function initSynonymIterator($batchSize = 1000) 1632 | { 1633 | return new SynonymIterator($this, $batchSize); 1634 | } 1635 | 1636 | /** 1637 | * @deprecated Please use searchForFacetValues instead 1638 | * @param $facetName 1639 | * @param $facetQuery 1640 | * @param array $query 1641 | * @return mixed 1642 | */ 1643 | public function searchFacet($facetName, $facetQuery, $query = array()) 1644 | { 1645 | return $this->searchForFacetValues($facetName, $facetQuery, $query); 1646 | } 1647 | 1648 | /** 1649 | * @param $params 1650 | * 1651 | * @return mixed 1652 | * 1653 | * @throws AlgoliaException 1654 | */ 1655 | public function searchRules(array $params = array()) 1656 | { 1657 | return $this->client->request( 1658 | $this->context, 1659 | 'POST', 1660 | '/1/indexes/'.$this->urlIndexName.'/rules/search', 1661 | null, 1662 | $params, 1663 | $this->context->readHostsArray, 1664 | $this->context->connectTimeout, 1665 | $this->context->readTimeout 1666 | ); 1667 | } 1668 | 1669 | /** 1670 | * @param $objectID 1671 | * 1672 | * @return mixed 1673 | * 1674 | * @throws AlgoliaException 1675 | */ 1676 | public function getRule($objectID) 1677 | { 1678 | return $this->client->request( 1679 | $this->context, 1680 | 'GET', 1681 | '/1/indexes/'.$this->urlIndexName.'/rules/'.urlencode($objectID), 1682 | null, 1683 | null, 1684 | $this->context->readHostsArray, 1685 | $this->context->connectTimeout, 1686 | $this->context->readTimeout 1687 | ); 1688 | } 1689 | 1690 | /** 1691 | * @param $objectID 1692 | * @param $forwardToReplicas 1693 | * 1694 | * @return mixed 1695 | * 1696 | * @throws AlgoliaException 1697 | */ 1698 | public function deleteRule($objectID, $forwardToReplicas = false) 1699 | { 1700 | return $this->client->request( 1701 | $this->context, 1702 | 'DELETE', 1703 | '/1/indexes/'.$this->urlIndexName.'/rules/'.urlencode($objectID).'?forwardToReplicas='.($forwardToReplicas ? 'true' : 'false'), 1704 | null, 1705 | null, 1706 | $this->context->writeHostsArray, 1707 | $this->context->connectTimeout, 1708 | $this->context->readTimeout 1709 | ); 1710 | } 1711 | 1712 | /** 1713 | * @param bool $forwardToReplicas 1714 | * 1715 | * @return mixed 1716 | * 1717 | * @throws AlgoliaException 1718 | */ 1719 | public function clearRules($forwardToReplicas = false) 1720 | { 1721 | return $this->client->request( 1722 | $this->context, 1723 | 'POST', 1724 | '/1/indexes/'.$this->urlIndexName.'/rules/clear?forwardToReplicas='.($forwardToReplicas ? 'true' : 'false'), 1725 | null, 1726 | null, 1727 | $this->context->writeHostsArray, 1728 | $this->context->connectTimeout, 1729 | $this->context->readTimeout 1730 | ); 1731 | } 1732 | 1733 | /** 1734 | * @param $rules 1735 | * @param bool $forwardToReplicas 1736 | * @param bool $clearExistingRules 1737 | * 1738 | * @return mixed 1739 | * 1740 | * @throws AlgoliaException 1741 | */ 1742 | public function batchRules($rules, $forwardToReplicas = false, $clearExistingRules = false) 1743 | { 1744 | return $this->client->request( 1745 | $this->context, 1746 | 'POST', 1747 | '/1/indexes/'.$this->urlIndexName.'/rules/batch?clearExistingRules='.($clearExistingRules ? 'true' : 'false') 1748 | .'&forwardToReplicas='.($forwardToReplicas ? 'true' : 'false'), 1749 | null, 1750 | $rules, 1751 | $this->context->writeHostsArray, 1752 | $this->context->connectTimeout, 1753 | $this->context->readTimeout 1754 | ); 1755 | } 1756 | 1757 | /** 1758 | * @param $objectID 1759 | * @param $content 1760 | * @param bool $forwardToReplicas 1761 | * 1762 | * @return mixed 1763 | * 1764 | * @throws AlgoliaException 1765 | */ 1766 | public function saveRule($objectID, $content, $forwardToReplicas = false) 1767 | { 1768 | if (!isset($content['objectID'])) { 1769 | $content['objectID'] = $objectID; 1770 | } 1771 | 1772 | return $this->client->request( 1773 | $this->context, 1774 | 'PUT', 1775 | '/1/indexes/'.$this->urlIndexName.'/rules/'.urlencode($objectID).'?forwardToReplicas='.($forwardToReplicas ? 'true' : 'false'), 1776 | null, 1777 | $content, 1778 | $this->context->writeHostsArray, 1779 | $this->context->connectTimeout, 1780 | $this->context->readTimeout 1781 | ); 1782 | } 1783 | 1784 | /** 1785 | * @param int $batchSize 1786 | * 1787 | * @return RuleIterator 1788 | */ 1789 | public function initRuleIterator($batchSize = 500) 1790 | { 1791 | return new RuleIterator($this, $batchSize); 1792 | } 1793 | 1794 | /** 1795 | * @param string $name 1796 | * @param array $arguments 1797 | * 1798 | * @return mixed 1799 | */ 1800 | public function __call($name, $arguments) 1801 | { 1802 | if ($name === 'browse') { 1803 | if (count($arguments) >= 1 && is_string($arguments[0])) { 1804 | return call_user_func_array(array($this, 'doBrowse'), $arguments); 1805 | } 1806 | 1807 | return call_user_func_array(array($this, 'doBcBrowse'), $arguments); 1808 | } 1809 | 1810 | throw new \BadMethodCallException(sprintf('No method named %s was found.', $name)); 1811 | } 1812 | } 1813 | -------------------------------------------------------------------------------- /vendor/algolia-client/src/AlgoliaSearch/IndexBrowser.php: -------------------------------------------------------------------------------- 1 | index = $index; 79 | $this->query = $query; 80 | $this->params = $params; 81 | 82 | $this->position = 0; 83 | 84 | $this->doQuery($cursor, $requestHeaders); 85 | } 86 | 87 | /** 88 | * @return mixed 89 | */ 90 | public function current() 91 | { 92 | return $this->hit; 93 | } 94 | 95 | /** 96 | * @return mixed 97 | */ 98 | public function next() 99 | { 100 | return $this->hit; 101 | } 102 | 103 | /** 104 | * @return int 105 | */ 106 | public function key() 107 | { 108 | return $this->position; 109 | } 110 | 111 | /** 112 | * @return bool 113 | */ 114 | public function valid() 115 | { 116 | do { 117 | if ($this->position < count($this->answer['hits'])) { 118 | $this->hit = $this->answer['hits'][$this->position]; 119 | $this->position++; 120 | 121 | return true; 122 | } 123 | 124 | if (isset($this->answer['cursor']) && $this->answer['cursor']) { 125 | $this->position = 0; 126 | 127 | $this->doQuery($this->answer['cursor']); 128 | 129 | continue; 130 | } 131 | 132 | return false; 133 | } while (true); 134 | } 135 | 136 | public function rewind() 137 | { 138 | $this->cursor = null; 139 | $this->position = 0; 140 | } 141 | 142 | /** 143 | * @return int 144 | */ 145 | public function cursor() 146 | { 147 | return $this->answer['cursor']; 148 | } 149 | 150 | /** 151 | * @param int $cursor 152 | * @param array $requestHeaders 153 | */ 154 | private function doQuery($cursor = null, $requestHeaders = array()) 155 | { 156 | if ($cursor !== null) { 157 | $this->params['cursor'] = $cursor; 158 | } 159 | 160 | $this->answer = $this->index->browseFrom($this->query, $this->params, $cursor, $requestHeaders); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /vendor/algolia-client/src/AlgoliaSearch/Iterators/AlgoliaIterator.php: -------------------------------------------------------------------------------- 1 | index = $index; 44 | $this->hitsPerPage = (int) $hitsPerPage; 45 | } 46 | 47 | /** 48 | * Return the current element 49 | * @return array 50 | */ 51 | public function current() 52 | { 53 | $this->ensureResponseExists(); 54 | $hit = $this->response['hits'][$this->getHitIndexForCurrentPage()]; 55 | 56 | return $this->formatHit($hit); 57 | } 58 | 59 | /** 60 | * Move forward to next element 61 | * @return void Any returned value is ignored. 62 | */ 63 | public function next() 64 | { 65 | $previousPage = $this->getCurrentPage(); 66 | $this->key++; 67 | if($this->getCurrentPage() !== $previousPage) { 68 | // Discard the response if the page has changed. 69 | $this->response = null; 70 | } 71 | } 72 | 73 | /** 74 | * Return the key of the current element 75 | * @return int 76 | */ 77 | public function key() 78 | { 79 | return $this->key; 80 | } 81 | 82 | /** 83 | * Checks if current position is valid. If the current position 84 | * is not valid, we call Algolia' API to load more results 85 | * until it's the last page. 86 | * 87 | * @return boolean The return value will be casted to boolean and then evaluated. 88 | * Returns true on success or false on failure. 89 | */ 90 | public function valid() 91 | { 92 | $this->ensureResponseExists(); 93 | 94 | return isset($this->response['hits'][$this->getHitIndexForCurrentPage()]); 95 | } 96 | 97 | /** 98 | * Rewind the Iterator to the first element 99 | * @return void Any returned value is ignored. 100 | */ 101 | public function rewind() 102 | { 103 | $this->key = 0; 104 | $this->response = null; 105 | } 106 | 107 | /** 108 | * ensureResponseExists is always called prior 109 | * to trying to access the response property. 110 | */ 111 | protected function ensureResponseExists() { 112 | if ($this->response === null) { 113 | $this->fetchCurrentPageResults(); 114 | } 115 | } 116 | 117 | /** 118 | * getCurrentPage returns the current zero based page according to 119 | * the current key and hits per page. 120 | * 121 | * @return int 122 | */ 123 | protected function getCurrentPage() 124 | { 125 | return (int) floor($this->key / ($this->hitsPerPage)); 126 | } 127 | 128 | /** 129 | * getHitIndexForCurrentPage retrieves the index 130 | * of the hit in the current page. 131 | * 132 | * @return int 133 | */ 134 | protected function getHitIndexForCurrentPage() 135 | { 136 | return $this->key - ($this->getCurrentPage() * $this->hitsPerPage); 137 | } 138 | 139 | /** 140 | * Call Algolia' API to get new result batch 141 | */ 142 | abstract protected function fetchCurrentPageResults(); 143 | 144 | /** 145 | * The export method might be is using search internally, this method 146 | * is used to clean the results, like remove the highlight 147 | * 148 | * @param array $hit 149 | * @return array formatted synonym array 150 | */ 151 | abstract protected function formatHit(array $hit); 152 | } 153 | -------------------------------------------------------------------------------- /vendor/algolia-client/src/AlgoliaSearch/Iterators/RuleIterator.php: -------------------------------------------------------------------------------- 1 | response = $this->index->searchRules(array( 35 | 'hitsPerPage' => $this->hitsPerPage, 36 | 'page' => $this->getCurrentPage(), 37 | )); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /vendor/algolia-client/src/AlgoliaSearch/Iterators/SynonymIterator.php: -------------------------------------------------------------------------------- 1 | response = $this->index->searchSynonyms('', array(), $this->getCurrentPage(), $this->hitsPerPage); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /vendor/algolia-client/src/AlgoliaSearch/Json.php: -------------------------------------------------------------------------------- 1 | context = $context; 49 | $this->client = $client; 50 | } 51 | 52 | /** 53 | * @param string $query 54 | * @param array|null $args 55 | * 56 | * @return mixed 57 | * 58 | * @throws AlgoliaException 59 | */ 60 | public function search($query, $args = null) 61 | { 62 | if ($args === null) { 63 | $args = array(); 64 | } 65 | $args['query'] = $query; 66 | 67 | return $this->client->request( 68 | $this->context, 69 | 'POST', 70 | '/1/places/query', 71 | array(), 72 | array('params' => $this->client->buildQuery($args)), 73 | $this->context->readHostsArray, 74 | $this->context->connectTimeout, 75 | $this->context->searchTimeout 76 | ); 77 | } 78 | 79 | /** 80 | * @param mixed $objectID 81 | * 82 | * @return mixed 83 | * 84 | * @throws AlgoliaException 85 | */ 86 | public function getObject($objectID) 87 | { 88 | return $this->client->request( 89 | $this->context, 90 | 'GET', 91 | '/1/places/' . urlencode($objectID), 92 | null, 93 | null, 94 | $this->context->readHostsArray, 95 | $this->context->connectTimeout, 96 | $this->context->searchTimeout 97 | ); 98 | } 99 | 100 | /** 101 | * @param string $key 102 | * @param string $value 103 | */ 104 | public function setExtraHeader($key, $value) 105 | { 106 | $this->context->setExtraHeader($key, $value); 107 | } 108 | 109 | /** 110 | * @return ClientContext 111 | */ 112 | public function getContext() 113 | { 114 | return $this->context; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /vendor/algolia-client/src/AlgoliaSearch/SynonymType.php: -------------------------------------------------------------------------------- 1 | 2 |
Number of indexable pages:
3 | 4 | -------------------------------------------------------------------------------- /widgets/algolia/algolia.php: -------------------------------------------------------------------------------- 1 | 12 | * @license MIT 13 | * @link https://getkirby.com 14 | */ 15 | 16 | return array( 17 | 'title' => [ 18 | 'text' => 'Search Index', 19 | 'compressed' => false 20 | ], 21 | 'options' => [ 22 | [ 23 | 'text' => 'Manual Refresh', 24 | 'icon' => 'refresh', 25 | 'link' => purl('widgets/algolia/index') 26 | ] 27 | ], 28 | 'html' => function() { 29 | $count = algolia()->objectCount(); 30 | return tpl::load(__DIR__ . DS . 'algolia.html.php', compact('count')); 31 | } 32 | ); 33 | --------------------------------------------------------------------------------